diff --git a/.claude/skills/hypercube-linear-context/SKILL.md b/.claude/skills/hypercube-linear-context/SKILL.md new file mode 100644 index 000000000..a751901e1 --- /dev/null +++ b/.claude/skills/hypercube-linear-context/SKILL.md @@ -0,0 +1,137 @@ +--- +name: hl-info +description: The mathematics of hypercube linear resolution. +--- + +# Hypercube Linear Resolution — Implementation Reference + +> Summary of *Resolution Meets Cutting Planes: Introducing Hypercube Linear Resolution* (Flippo, Stuckey, Demirović — CPAIOR '26). +> Use as context when implementing the proof system, propagator, or conflict analysis. + +## 1. Core Idea + +A **hypercube linear constraint** is a unification of a clause and an integer linear inequality: + +``` +H → Σ wᵢ·xᵢ ≤ r +``` + +where `H` is a *hypercube* (a consistent conjunction of atomic constraints). + +- If `wᵢ = 0` for all `i` and `r ≤ -1` → degenerates to a **clause** (`H → ⊥`). +- If `D ⊨ H` (hypercube entailed by the current domain) → behaves as the plain linear inequality `Σ wᵢxᵢ ≤ r`. +- Generalises **indicator constraints** (single 0-1 bound in `H`). +- Variables may appear in *both* `H` and the linear part — no separation required. + +The proof system, **hypercube linear resolution (HLR)**, is **sound and complete** for integer linear reasoning. It can be viewed as an extended cutting-planes system where extended variables are bound constraints. + +--- + +## 2. Preliminaries / Data Model + +### Hypercube `H` +- A **consistent** conjunction of atomic constraints (e.g. no `⟨x ≥ l⟩ ∧ ⟨x ≤ u⟩` with `l > u`). +- May implicitly include the variable's full domain bounds without changing meaning. +- `D_H` denotes the domain induced by `H`. + +### Linear constraint `Σ wᵢxᵢ ≤ r` slack +``` +slack(D, R) = r − Σ lb(D, wᵢxᵢ) +lb(D, wᵢxᵢ) = wᵢ · lb(D, xᵢ) if wᵢ ≥ 0 + = wᵢ · ub(D, xᵢ) if wᵢ < 0 +``` + +--- + +## 3. Hypercube Linear Slack + +**Key definition** (Definition 2): +``` +hlslack(D, L) = r − Σ max( lb(D, wᵢxᵢ), lb(D_H, wᵢxᵢ) ) +``` + +Takes the *tighter* of the current domain bound or the hypercube bound for each variable contribution. Rationale: once `H` becomes entailed, `D_H`'s bounds will hold, so we can already use them when reasoning about the linear component. + +- If `D ⊨ H` then `hlslack(D, H → R) = slack(D, R)`. +- Conflict detection: `L` is conflicting w.r.t. `D` iff `D ⊨ H` AND `slack(D, R) < 0`. + +--- + +## 4. Hypercube Linear Propagator + +For `L ≡ H → R` under domain `D`, three propagation rules: + +### Rule 1 — Hypercube fully entailed +If `D ⊨ H`: propagate `R` as a standard linear constraint. + +### Rule 2 — Negative slack, one hypercube bound unassigned +If `hlslack(D, L) < 0` and exactly one bound `c ∈ H` is unsatisfied, propagate `¬c`: +- `c = ⟨x ≤ d⟩` → propagate `x ≥ d + 1` +- `c = ⟨x ≥ d⟩` → propagate `x ≤ d − 1` + +### Rule 3 — Non-negative slack, one hypercube bound unassigned +If `hlslack(D, L) ≥ 0` and exactly one `c ∈ H` over variable `x` (with linear coefficient `w`) is unassigned, you can still propagate a *weaker* bound: +- `c ≡ ⟨x ≥ d⟩`, `w > 0` → `x ≤ ⌊(hlslack(D, L) + w·d) / w⌋` +- `c ≡ ⟨x ≤ d⟩`, `w < 0` → `x ≥ ⌈(hlslack(D, L) + w·d) / w⌉` + +**Reason**: in the case `c` is true, `R` enforces a tighter bound; in the case `¬c`, the bound is implied directly. The weaker bound is true in either case. + +### Explanations +Every propagated bound `c` carries an explanation `B` — the conjunction of bounds that caused it. Same concept as in LCG solvers. + +--- + +## 5. Inference Rules of HLR + +Four rules: `Weaken`, `Divide`, `ResF` (Fourier), `ResH` (propositional). + +### 5.1 Weakening (Definition 3) +Move part of a coefficient from the linear term into the hypercube. + +``` +H → Σ wᵢxᵢ ≤ r +───────────────────────────────────────────── (lower bound) +H ∧ ⟨xⱼ ≥ d⟩ → Σᵢ≠ⱼ wᵢxᵢ + (wⱼ−1)xⱼ ≤ r − d + +H → Σ wᵢxᵢ ≤ r +───────────────────────────────────────────── (upper bound) +H ∧ ⟨xⱼ ≤ d⟩ → Σᵢ≠ⱼ wᵢxᵢ + (wⱼ+1)xⱼ ≤ r + d +``` + +**Full elimination of `xⱼ`** = apply weakening `|wⱼ|` times, ending at: +- `wⱼ > 0`: `H ∧ ⟨xⱼ ≥ d⟩ → Σᵢ≠ⱼ wᵢxᵢ ≤ r − wⱼd` +- `wⱼ < 0`: `H ∧ ⟨xⱼ ≤ d⟩ → Σᵢ≠ⱼ wᵢxᵢ ≤ r + wⱼd` + +**Slack-preserving property (Proposition 3)**: weakening on the *current* domain bound `⟨x ≥ lb(D,x)⟩` (or `⟨x ≤ ub(D,x)⟩`) does **not change `hlslack(D, L)`**. This is critical for keeping the conflict alive during analysis. + +### 5.2 Division +``` +H → Σ wᵢxᵢ ≤ r (d > 0) +──────────────────────────────── +H → Σ ⌊wᵢ/d⌋ xᵢ ≤ ⌊r/d⌋ +``` +Sound (standard rounded division on integer inequalities). Not required for completeness but prevents coefficient overflow. + +### 5.3 Fourier Resolution `ResF` +For `L₁ ≡ H₁ → R₁`, `L₂ ≡ H₂ → R₂` where `xⱼ` has positive weight `a` in `R₁` and negative weight `b` in `R₂` (so `a·b < 0`), and `H₁ ∧ H₂` is consistent: + +``` +H₁ → R₁ H₂ → R₂ +────────────────────────────── +H₁ ∧ H₂ → FR(R₁, R₂, xⱼ) +``` + +where `FR(R₁, R₂, xⱼ) = Σᵢ≠ⱼ (−w'ⱼ·wᵢ + wⱼ·w'ᵢ) xᵢ ≤ −w'ⱼ·r + wⱼ·r'`, eliminating `xⱼ`. + +### 5.4 Propositional-style Resolution `ResH` +For two hypercube linears with bounds `c₁ ≡ ⟨x ≥ d₁⟩ ∈ H₁` and `c₂ ≡ ⟨x ≤ d₂⟩ ∈ H₂` such that `d₁ ≤ d₂ + 1` (covers all values of `x`), and `H'₁ ∧ H'₂` consistent (`Hₖ ≡ H'ₖ ∧ cₖ`): + +The rule is parameterised as `ResH_F`. The default instantiation requires `L₂` to be reduced to a clause first (linear part `⊥`): +``` +H'₁ ∧ c₁ → R₁ H'₂ ∧ c₂ → ⊥ +───────────────────────────────── +H'₁ ∧ H'₂ → R₁ +``` +Soundness relies on: at least one of `c₁`, `c₂` is true for every value of `x`; weakening `L₂` to a clause is always possible. + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..b219ce2ac --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "uv" + directory: "pumpkin-solver-py/" + schedule: + interval: "weekly" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml index e13b5b7c5..9451e5d23 100644 --- a/.github/workflows/ci-python.yml +++ b/.github/workflows/ci-python.yml @@ -38,8 +38,8 @@ jobs: - runner: ubuntu-22.04 target: armv7 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: 3.x - name: Build wheels @@ -52,7 +52,7 @@ jobs: manylinux: auto docker-options: -e NO_CHECKERS - name: Upload wheels - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: wheels-linux-${{ matrix.platform.target }} path: pumpkin-solver-py/dist @@ -71,8 +71,8 @@ jobs: - runner: ubuntu-22.04 target: armv7 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: 3.x - name: Build wheels @@ -84,7 +84,7 @@ jobs: sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} manylinux: musllinux_1_2 - name: Upload wheels - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: wheels-musllinux-${{ matrix.platform.target }} path: pumpkin-solver-py/dist @@ -99,8 +99,8 @@ jobs: - runner: windows-latest target: x86 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: 3.x architecture: ${{ matrix.platform.target }} @@ -112,7 +112,7 @@ jobs: args: --release --out dist --find-interpreter sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - name: Upload wheels - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: wheels-windows-${{ matrix.platform.target }} path: pumpkin-solver-py/dist @@ -127,8 +127,8 @@ jobs: - runner: macos-latest target: aarch64 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: 3.x - name: Build wheels @@ -139,7 +139,7 @@ jobs: args: --release --out dist --find-interpreter sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - name: Upload wheels - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: wheels-macos-${{ matrix.platform.target }} path: pumpkin-solver-py/dist @@ -147,7 +147,7 @@ jobs: sdist: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Build sdist uses: PyO3/maturin-action@v1 with: @@ -155,7 +155,7 @@ jobs: command: sdist args: --out dist - name: Upload sdist - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: wheels-sdist path: pumpkin-solver-py/dist @@ -174,9 +174,9 @@ jobs: # Used to generate artifact attestation attestations: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 - name: Generate artifact attestation - uses: actions/attest-build-provenance@v2 + uses: actions/attest-build-provenance@v4 with: subject-path: 'wheels-*/*' - name: Publish to PyPI diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d919643a..48a8e3ab6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,8 +15,8 @@ jobs: name: Test Suite runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/cache@v4 + - uses: actions/checkout@v6 + - uses: actions/cache@v5 with: path: | ~/.cargo/bin/ @@ -26,13 +26,13 @@ jobs: target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - uses: dtolnay/rust-toolchain@stable - - run: cargo test --release --no-fail-fast --features pumpkin-solver/check-propagations + - run: cargo test --release --no-fail-fast --features pumpkin-solver/check-propagations --features pumpkin-core/check-deductions wasm-test: name: Test Suite for pumpkin-core in WebAssembly runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable with: targets: wasm32-unknown-unknown @@ -48,8 +48,8 @@ jobs: runner: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.runner }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: 3.x - name: Install pytest @@ -65,8 +65,8 @@ jobs: name: Documentation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/cache@v4 + - uses: actions/checkout@v6 + - uses: actions/cache@v5 with: path: | ~/.cargo/bin/ @@ -82,8 +82,8 @@ jobs: name: Code Style and Lints runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/cache@v4 + - uses: actions/checkout@v6 + - uses: actions/cache@v5 with: path: | ~/.cargo/bin/ @@ -102,5 +102,5 @@ jobs: name: Dependency Licensing runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: EmbarkStudios/cargo-deny-action@v2 diff --git a/.github/workflows/reset_reviewed_status.yml b/.github/workflows/reset_reviewed_status.yml index 3da72b0f2..d443385c8 100644 --- a/.github/workflows/reset_reviewed_status.yml +++ b/.github/workflows/reset_reviewed_status.yml @@ -12,7 +12,7 @@ jobs: pull-requests: write steps: - name: Dismiss old reviews - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const reviews = await github.paginate(github.rest.pulls.listReviews, { diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..61898f246 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +Pumpkin is a constraint programming (CP) solver written in pure Rust, based on the lazy clause generation paradigm. It supports proof logging via DRAT (SAT) and DRCP (CP) certificates. + +## Build & Development Commands + +```bash +# Build +cargo build +cargo build --release + +# Test (standard CI command) +cargo test --release --no-fail-fast --features pumpkin-solver/check-propagations + +# Run a single test +cargo test --release --features pumpkin-solver/check-propagations + +# Format (requires nightly) +cargo +nightly fmt +cargo +nightly fmt --check # check only + +# Lint (requires nightly) +cargo +nightly clippy --all-targets -- -Dwarnings + +# Docs +cargo doc --no-deps + +# License check +cargo deny check + +# WASM tests (for pumpkin-crates/core) +wasm-pack test --release --node + +# Python tests +cd pumpkin-solver-py && pytest +``` + +Scripts in `scripts/` (`fmt.sh`, `clippy.sh`, `documentation.sh`, `deny.sh`) mirror the CI commands and are also run by the pre-commit hook (`.githooks/pre-commit`). + +## Workspace Architecture + +The project is a Cargo workspace (edition 2024, resolver 2). Crates are grouped by role: + +### Core Engine (`pumpkin-crates/`) +- **`core`** — The main solver engine. Contains the CDCL loop, propagation engine, nogood learning, branching heuristics, and proof logging infrastructure. This is the heart of the solver. +- **`checking`** — Shared types used by both `core` and `pumpkin-checker` (avoids circular deps). +- **`propagators`** — Implementations of CP propagators (arithmetic, cumulative, disjunctive, element, etc.). +- **`conflict-resolvers`** — Pluggable conflict analysis strategies for nogood derivation. +- **`constraints`** — High-level constraint API built on top of `core`. + +### Interfaces +- **`pumpkin-solver`** — CLI binary. Accepts CNF, WCNF (MaxSAT), and FlatZinc input formats. +- **`pumpkin-solver-py`** — Python bindings via PyO3. + +### Proof Infrastructure +- **`drcp-format`** — Reading/writing the DRCP proof certificate format. +- **`pumpkin-checker`** — Standalone proof verification tool. +- **`pumpkin-proof-processor`** — Proof transformation/preprocessing utility. +- **`drcp-debugger`** — Debugging tool for DRCP proofs. + +### Parsing & Utilities +- **`fzn-rs`** / **`fzn-rs-derive`** — FlatZinc parser and derive macros. +- **`pumpkin-macros`** — Procedural macros used across the workspace. +- **`minizinc/`** — MiniZinc integration (solver plugin). + +## Key Design Concepts + +- **Lazy Clause Generation (LCG)**: The solver operates on a hybrid SAT/CP model. CP propagators generate explanations (clauses/nogoods) on demand during conflict analysis. +- **Proof Logging**: Every inference can be certified. The `core` crate threads proof-logging through propagators via a `Proof` type. The `check-propagations` feature enables runtime validation of propagator explanations during tests. +- **Propagator Interface**: Custom propagators implement the `Propagator` trait in `pumpkin-crates/core`. They must provide both `propagate` and `explain` methods for proof soundness. +- **Feature Flags**: `pumpkin-solver/check-propagations` enables expensive correctness assertions — always enable this when running tests. + +## Code Style + +- **Rust edition 2024**, stable toolchain (see `rust-toolchain.toml`), but formatting/linting requires nightly. +- Line length: 120 chars for comments (`rustfmt.toml`). +- Imports: grouped (std / external / crate), single-line style enforced. +- Workspace-level lints are configured in the root `Cargo.toml` — check there before suppressing warnings locally. diff --git a/Cargo.lock b/Cargo.lock index cd058e9e9..fe86a51a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -40,15 +40,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -59,7 +59,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -70,14 +70,14 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "ar_archive_writer" @@ -90,9 +90,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.1.2" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" dependencies = [ "anstyle", "bstr", @@ -122,21 +122,41 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitfield" -version = "0.14.0" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ba6517c6b0f2bf08be60e187ab64b038438f22dd755614d8fe4d4098c46419" +dependencies = [ + "bitfield-macros", +] + +[[package]] +name = "bitfield-macros" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" +checksum = "f48d6ace212fdf1b45fd6b566bb40808415344642b76c3224c07c8df9da81e97" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "bitfield-struct" -version = "0.9.5" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2869c63ccf4f8bf0d485070b880e60e097fb7aeea80ee82a0a94a957e372a0b" +checksum = "3ca6739863c590881f038d033a146c51ddae239186a4327014839fd864f44ed5" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + [[package]] name = "bstr" version = "1.12.1" @@ -150,9 +170,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "cast" @@ -162,9 +182,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "shlex", @@ -176,11 +196,22 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + [[package]] name = "chumsky" -version = "0.10.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14377e276b2c8300513dff55ba4cc4142b44e5d6de6d00eb5b2307d650bb4ec1" +checksum = "4ba4a05c9ce83b07de31b31c874e87c069881ac4355db9e752e3a55c11ec75a6" dependencies = [ "hashbrown 0.15.5", "regex-automata 0.3.9", @@ -192,9 +223,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.57" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -212,9 +243,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.57" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -224,9 +255,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -236,25 +267,34 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "convert_case" -version = "0.8.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -327,9 +367,9 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "downcast-rs" -version = "1.2.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" [[package]] name = "drcp-debugger" @@ -402,32 +442,19 @@ dependencies = [ [[package]] name = "env_filter" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.10.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ - "humantime", - "is-terminal", "log", "regex", - "termcolor", ] [[package]] name = "env_logger" -version = "0.11.9" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", @@ -449,7 +476,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -501,26 +528,25 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-task", "pin-project-lite", - "pin-utils", "slab", ] @@ -546,14 +572,17 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.17" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "r-efi", + "rand_core", + "wasip2", + "wasip3", "wasm-bindgen", ] @@ -570,9 +599,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -581,16 +610,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "humantime" +name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "ident_case" @@ -600,32 +623,14 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", -] - -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - -[[package]] -name = "is-terminal" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.61.2", + "hashbrown 0.17.0", + "serde", + "serde_core", ] [[package]] @@ -636,24 +641,24 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.20" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "log", @@ -664,9 +669,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.20" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", @@ -675,19 +680,33 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.181" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libm" @@ -702,19 +721,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] -name = "memchr" -version = "2.8.0" +name = "matchers" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata 0.4.14", +] [[package]] -name = "memoffset" -version = "0.9.1" +name = "memchr" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "minicov" @@ -742,7 +761,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -830,9 +849,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -847,16 +866,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] -name = "pin-project-lite" -version = "0.2.16" +name = "paste" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] -name = "pin-utils" -version = "0.1.0" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "portable-atomic" @@ -866,27 +885,18 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib", @@ -895,20 +905,30 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -920,9 +940,9 @@ dependencies = [ [[package]] name = "psm" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" dependencies = [ "ar_archive_writer", "cc", @@ -952,6 +972,7 @@ version = "0.3.0" dependencies = [ "dyn-clone", "fnv", + "thiserror", ] [[package]] @@ -992,6 +1013,7 @@ dependencies = [ "once_cell", "pumpkin-checking", "rand", + "test-log", "thiserror", "wasm-bindgen-test", "web-time", @@ -1017,7 +1039,7 @@ dependencies = [ "clap", "clap-verbosity-flag", "drcp-format", - "env_logger 0.11.9", + "env_logger", "escargot", "flate2", "fzn-rs", @@ -1047,9 +1069,11 @@ version = "0.3.0" dependencies = [ "cc", "clap", - "env_logger 0.10.2", + "clap-verbosity-flag", + "env_logger", "flatzinc", "log", + "paste", "pumpkin-checker", "pumpkin-conflict-resolvers", "pumpkin-constraints", @@ -1076,36 +1100,32 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.25.1" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" +checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" dependencies = [ - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.25.1" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" +checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.25.1" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" +checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" dependencies = [ "libc", "pyo3-build-config", @@ -1113,9 +1133,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.25.1" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" +checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -1125,9 +1145,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.25.1" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" +checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" dependencies = [ "heck", "proc-macro2", @@ -1138,42 +1158,35 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] -name = "rand" -version = "0.8.5" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "rand" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "ppv-lite86", + "chacha20", + "getrandom", "rand_core", ] [[package]] name = "rand_core" -version = "0.6.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "regex" @@ -1184,7 +1197,7 @@ dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.14", - "regex-syntax 0.8.9", + "regex-syntax 0.8.10", ] [[package]] @@ -1206,7 +1219,7 @@ checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.9", + "regex-syntax 0.8.10", ] [[package]] @@ -1217,9 +1230,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rustc_version" @@ -1247,9 +1260,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1294,6 +1307,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1302,9 +1324,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" -version = "0.3.18" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" dependencies = [ "libc", "signal-hook-registry", @@ -1322,9 +1344,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "slab" @@ -1334,22 +1356,22 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "stacker" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" dependencies = [ "cc", "cfg-if", "libc", "psm", - "windows-sys 0.59.0", + "windows-sys", ] [[package]] name = "stringcase" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04028eeb851ed08af6aba5caa29f2d59a13ed168cee4d6bd753aeefcf1d636b0" +checksum = "72abeda133c49d7bddece6c154728f83eec8172380c80ab7096da9487e20d27c" [[package]] name = "strsim" @@ -1359,9 +1381,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1370,24 +1392,47 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.4" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "termtree" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1dd07eb858a2067e2f3c7155d54e929265c264e6f37efe3ee7a8d1b5a1dd0ba" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] -name = "termcolor" -version = "1.4.1" +name = "test-log" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +checksum = "2f46bf474f0a4afebf92f076d54fd5e63423d9438b8c278a3d2ccb0f47f7cdb3" dependencies = [ - "winapi-util", + "env_logger", + "test-log-macros", + "tracing-subscriber", ] [[package]] -name = "termtree" -version = "0.5.1" +name = "test-log-core" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +checksum = "37d4d41320b48bc4a211a9021678fcc0c99569b594ea31c93735b8e517102b4c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "test-log-macros" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9beb9249a81e430dffd42400a49019bcf548444f1968ff23080a625de0d4d320" +dependencies = [ + "syn", + "test-log-core", +] [[package]] name = "thiserror" @@ -1409,23 +1454,80 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata 0.4.14", + "sharded-slab", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] -name = "unindent" -version = "0.2.4" +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "utf8parse" @@ -1433,6 +1535,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "wait-timeout" version = "0.2.1" @@ -1453,16 +1561,28 @@ dependencies = [ ] [[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "wasip2" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -1473,23 +1593,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1497,9 +1613,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -1510,18 +1626,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.58" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45649196a53b0b7a15101d845d44d2dda7374fc1b5b5e2bbf58b7577ff4b346d" +checksum = "29826f9d9ecaa314c480d376b276d1c790e6cb6a4681fab8532da69cbabf977d" dependencies = [ "async-trait", "cast", @@ -1541,9 +1657,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.58" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f579cdd0123ac74b94e1a4a72bd963cf30ebac343f2df347da0b8df24cdebed2" +checksum = "c610311887f9e6599a546d278d12d69dfd3a3e92639b2129e4b11ad6cf1961d6" dependencies = [ "proc-macro2", "quote", @@ -1552,18 +1668,42 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-shared" -version = "0.2.108" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8145dd1593bf0fb137dbfa85b8be79ec560a447298955877804640e40c2d6ea" +checksum = "60238e5b4b1b295701d6f9a66d2a126fe19990348f5fb9dae3b623a370119d94" [[package]] -name = "web-sys" -version = "0.3.85" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ - "js-sys", - "wasm-bindgen", + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", ] [[package]] @@ -1582,7 +1722,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -1591,15 +1731,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -1610,100 +1741,110 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "winnow" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "memchr", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] [[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] [[package]] -name = "winnow" -version = "0.6.26" +name = "wit-bindgen-rust-macro" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ - "memchr", + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] -name = "zerocopy" -version = "0.8.39" +name = "wit-component" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ - "zerocopy-derive", + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", ] [[package]] -name = "zerocopy-derive" -version = "0.8.39" +name = "wit-parser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ - "proc-macro2", - "quote", - "syn", + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] [[package]] name = "zmij" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/clippy.toml b/clippy.toml index c6035472e..dc9345fa7 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,6 +1,5 @@ allowed-duplicate-crates = [ - "hashbrown", - "windows-sys", - "regex-syntax", + "wit-bindgen", "regex-automata", + "regex-syntax", ] diff --git a/deny.toml b/deny.toml index 57446cec5..df9bbcdc3 100644 --- a/deny.toml +++ b/deny.toml @@ -94,6 +94,7 @@ allow = [ "Apache-2.0 WITH LLVM-exception", "Zlib", "Unicode-3.0", + "BSD-3-Clause", ] # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the diff --git a/fzn-rs-derive/Cargo.toml b/fzn-rs-derive/Cargo.toml index c3ab0ccf7..df88c76dd 100644 --- a/fzn-rs-derive/Cargo.toml +++ b/fzn-rs-derive/Cargo.toml @@ -11,7 +11,7 @@ authors.workspace = true proc-macro = true [dependencies] -convert_case = "0.8.0" +convert_case = "0.11.0" proc-macro2 = "1.0.95" quote = "1.0.40" syn = { version = "2.0.104", features = ["extra-traits"] } diff --git a/fzn-rs/Cargo.toml b/fzn-rs/Cargo.toml index e44585d4b..e7375a9f8 100644 --- a/fzn-rs/Cargo.toml +++ b/fzn-rs/Cargo.toml @@ -8,7 +8,7 @@ license.workspace = true authors.workspace = true [dependencies] -chumsky = { version = "0.10.1" } +chumsky = { version = "0.12.0" } thiserror = "2.0.12" fzn-rs-derive = { version = "0.1.0", path = "../fzn-rs-derive/" } diff --git a/hl-scripts/.python-version b/hl-scripts/.python-version new file mode 100644 index 000000000..24ee5b1be --- /dev/null +++ b/hl-scripts/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/hl-scripts/README.md b/hl-scripts/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/hl-scripts/hlproofchecker.py b/hl-scripts/hlproofchecker.py new file mode 100644 index 000000000..336591c3a --- /dev/null +++ b/hl-scripts/hlproofchecker.py @@ -0,0 +1,516 @@ +#!/usr/bin/env python3 +""" +Check a Hypercube Linear (HL) proof file using the Z3 SMT solver, and +optionally verify that each axiom is implied by the FlatZinc model. + +The proof file contains these line types: + v - variable name mapping: v + i - axiom + di - intermediate derived state (written at each loop iteration of the resolver) + d - final derived constraint + +For each 'di' step a Z3 query is issued using: + - all 'i' steps accumulated since the last 'd' (or start), plus + - the most recent 'di' step in the current group (reset by each 'd'), if any. + +For each 'd' step a query is issued using the same set of parents, then the +group is reset. + + - unsat => derivation is correct + - sat => a counterexample was found; the derivation may be wrong + +Axiom verification (--fzn): + For each 'i' axiom H -> R, checks that M ∧ H ∧ ¬R is UNSAT using Pumpkin + with the default conflict resolver. + +Usage: + python3 hlproofchecker.py proof.hl + python3 hlproofchecker.py proof.hl --fzn model.fzn --solver ./pumpkin-solver +""" + +import argparse +import os +import re +import subprocess +import sys +import tempfile +from dataclasses import dataclass + +import z3 + + +# --------------------------------------------------------------------------- +# Data structures +# --------------------------------------------------------------------------- + + +@dataclass +class Predicate: + var: str + op: str # '==', '!=', '<=', '>=' + val: int + + +@dataclass +class LinearInequality: + terms: list # list of (int coeff, str var) + bound: int + trivially_false: bool = False + + +@dataclass +class HLStep: + kind: str # 'i', 'di', or 'd' + step_id: int | None # integer ID for 'd' steps, None for 'i'/'di' steps + predicates: list # list[Predicate] + linear: LinearInequality + line_num: int # 1-based line number in source file + + +# --------------------------------------------------------------------------- +# Parsing +# --------------------------------------------------------------------------- + +_PRED_RE = re.compile(r"\[(\w+)\s*(==|!=|<=|>=)\s*(-?\d+)\]") + + +def parse_predicate(s: str) -> Predicate: + m = _PRED_RE.match(s.strip()) + if not m: + raise ValueError(f"Cannot parse predicate: {s!r}") + return Predicate(var=m.group(1), op=m.group(2), val=int(m.group(3))) + + +def parse_linear(s: str) -> LinearInequality: + """Parse the right-hand side of '->': e.g. ' 1 x1 -1 x3 <= 0' or ' <= -1'.""" + s = s.strip() + idx = s.rfind("<=") + if idx < 0: + raise ValueError(f"No '<=' in linear part: {s!r}") + lhs = s[:idx].strip() + bound = int(s[idx + 2 :].strip()) + + terms = [] + if lhs: + tokens = lhs.split() + if len(tokens) % 2 != 0: + raise ValueError(f"Odd number of tokens in linear LHS: {lhs!r}") + for i in range(0, len(tokens), 2): + coeff = int(tokens[i]) + var = tokens[i + 1] + terms.append((coeff, var)) + + trivially_false = not terms and bound < 0 + return LinearInequality(terms=terms, bound=bound, trivially_false=trivially_false) + + +def parse_line(line_num: int, raw: str) -> HLStep: + """Parse one line of the proof file into an HLStep.""" + raw = raw.strip() + raw = re.sub(r'\s+confl@\d+\s+bt=\d+\s*$', '', raw) + if not raw: + raise ValueError("Empty line") + + parts = raw.split("->", 1) + if len(parts) != 2: + raise ValueError(f"Line {line_num}: missing '->' in: {raw!r}") + + prefix = parts[0].strip() + linear = parse_linear(parts[1]) + + if prefix == "i" or prefix.startswith("i "): + kind = "i" + step_id = None + pred_str = prefix[1:].strip() + elif prefix == "di" or prefix.startswith("di "): + kind = "di" + step_id = None + pred_str = prefix[2:].strip() + elif prefix.startswith("d "): + kind = "d" + rest = prefix[2:].strip() + id_and_preds = rest.split(None, 1) + step_id = int(id_and_preds[0]) + pred_str = id_and_preds[1].strip() if len(id_and_preds) > 1 else "" + else: + raise ValueError(f"Line {line_num}: unknown prefix in: {raw!r}") + + predicates = [] + if pred_str: + for chunk in re.split(r"\s*&\s*", pred_str): + chunk = chunk.strip() + if chunk: + predicates.append(parse_predicate(chunk)) + + return HLStep( + kind=kind, + step_id=step_id, + predicates=predicates, + linear=linear, + line_num=line_num, + ) + + +def _collect_var_map(path: str) -> dict[str, str]: + """Streaming pass to extract the var_map (solver ID → FlatZinc name).""" + var_map: dict[str, str] = {} + with open(path) as f: + for raw in f: + parts = raw.split() + if parts and parts[0] == "v" and len(parts) == 3: + var_map[parts[1]] = parts[2] + return var_map + + +def iter_steps(path: str): + """Generator that yields HLStep objects, skipping 'v' lines.""" + with open(path) as f: + for line_num, raw in enumerate(f, 1): + raw = raw.rstrip("\n") + if not raw.strip(): + continue + tokens = raw.split(None, 1) + if tokens[0] == "v": + continue + yield parse_line(line_num, raw) + + +# --------------------------------------------------------------------------- +# Z3 helpers +# --------------------------------------------------------------------------- + + +def _z3_var(name: str, cache: dict) -> z3.ArithRef: + """Return (or create and cache) the Z3 Int variable for `name`.""" + if name not in cache: + cache[name] = z3.Int(name) + return cache[name] + + +def _z3_predicate(pred: Predicate, cache: dict) -> z3.BoolRef: + v = _z3_var(pred.var, cache) + if pred.op == "==": + return v == pred.val + if pred.op == "!=": + return v != pred.val + if pred.op == "<=": + return v <= pred.val + if pred.op == ">=": + return v >= pred.val + raise ValueError(f"Unknown predicate op: {pred.op!r}") + + +def _z3_linear_holds(linear: LinearInequality, cache: dict) -> z3.BoolRef: + if linear.trivially_false: + return z3.BoolVal(False) + if not linear.terms and linear.bound >= 0: + return z3.BoolVal(True) + lhs = z3.Sum([c * _z3_var(v, cache) for c, v in linear.terms]) + return lhs <= linear.bound + + +def _z3_axiom(step: HLStep, cache: dict) -> z3.BoolRef: + """Return the formula H ⇒ L for `step`.""" + l = _z3_linear_holds(step.linear, cache) + if not step.predicates: + return l + h = z3.And([_z3_predicate(p, cache) for p in step.predicates]) + return z3.Implies(h, l) + + +# --------------------------------------------------------------------------- +# Human-readable step label (for progress / error messages) +# --------------------------------------------------------------------------- + + +def _hl_str(step: HLStep) -> str: + h = " & ".join(f"[{p.var} {p.op} {p.val}]" for p in step.predicates) + l = ( + f"<= {step.linear.bound}" + if not step.linear.terms + else " ".join(f"{c} {v}" for c, v in step.linear.terms) + + f" <= {step.linear.bound}" + ) + return f"{h} -> {l}" + + +def _step_label(step: HLStep) -> str: + if step.kind == "di": + return f"di (line {step.line_num})" + return f"d ID={step.step_id} (line {step.line_num})" + + +# --------------------------------------------------------------------------- +# Z3 proof checking — streaming, single pass +# --------------------------------------------------------------------------- + +_PROGRESS_WIDTH = 60 # dots per line before wrapping + + +def run_z3_checks(proof_path: str) -> bool: + """Stream through the proof file and check each derivation step with Z3. + + Variables are declared lazily as they are first encountered. + Only the current proof group is held in memory at any time. + + Returns True if all checks pass, False if any check fails. + """ + solver = z3.Solver() + vars_cache: dict[str, z3.ArithRef] = {} + + current_i_group: list[HLStep] = [] + current_di: HLStep | None = None + check_num = n_fail = 0 + col = 0 # current column in the progress line + + def _progress_ok() -> None: + nonlocal col + sys.stderr.write(".") + col += 1 + if col >= _PROGRESS_WIDTH: + sys.stderr.write(f" {check_num}\n") + col = 0 + sys.stderr.flush() + + def _progress_fail(label: str) -> None: + nonlocal col + if col > 0: + sys.stderr.write("\n") + col = 0 + sys.stderr.write(f"FAIL: {label}\n") + sys.stderr.flush() + + def _check(parents: list[HLStep], target: HLStep) -> bool: + solver.push() + for parent in parents: + solver.add(_z3_axiom(parent, vars_cache)) + for pred in target.predicates: + solver.add(_z3_predicate(pred, vars_cache)) + solver.add(z3.Not(_z3_linear_holds(target.linear, vars_cache))) + result = solver.check() + solver.pop() + return result == z3.unsat + + for step in iter_steps(proof_path): + if step.kind == "i": + current_i_group.append(step) + + elif step.kind == "di": + check_num += 1 + parents = current_i_group + ([current_di] if current_di is not None else []) + ok = _check(parents, step) + if ok: + _progress_ok() + else: + n_fail += 1 + _progress_fail(_step_label(step)) + current_di = step + current_i_group = [] + + else: # 'd' + check_num += 1 + parents = current_i_group + ([current_di] if current_di is not None else []) + ok = _check(parents, step) + if ok: + _progress_ok() + else: + n_fail += 1 + _progress_fail(_step_label(step)) + current_di = None + current_i_group = [] + + # Finish the progress line if it has partial content. + if col > 0: + sys.stderr.write(f" {check_num}\n") + + n_ok = check_num - n_fail + sys.stderr.write( + f"Derivation checks: {n_ok} ok, {n_fail} failed" + f" ({check_num} total)\n" + ) + return n_fail == 0 + + +# --------------------------------------------------------------------------- +# FlatZinc axiom verification +# --------------------------------------------------------------------------- + + +def _fzn_var(xN: str, var_map: dict) -> str: + """Translate a solver variable ID to its FlatZinc name, falling back to xN.""" + return var_map.get(xN, xN) + + +def _fzn_predicate(pred: Predicate, var_map: dict) -> str: + """FlatZinc constraint line asserting a single predicate.""" + fname = _fzn_var(pred.var, var_map) + if pred.op == "<=": + return f"constraint int_le({fname}, {pred.val});" + if pred.op == ">=": + return f"constraint int_le({pred.val}, {fname});" + if pred.op == "==": + return f"constraint int_eq({fname}, {pred.val});" + if pred.op == "!=": + return f"constraint int_ne({fname}, {pred.val});" + raise ValueError(f"Unknown predicate op: {pred.op!r}") + + +def _fzn_negated_linear(linear: LinearInequality, var_map: dict) -> str | None: + """FlatZinc constraint for ¬(linear holds). + + Returns None if the negation is trivially false (nothing to assert; + the check is vacuously UNSAT — skip it). + Returns "" if the negation is trivially true (no constraint needed; + only the hypercube conditions are checked). + Returns a constraint string otherwise. + """ + if linear.trivially_false: + # linear is always false; ¬(always false) = true → no constraint needed + return "" + if not linear.terms and linear.bound >= 0: + # linear is always true; ¬(always true) = false → trivially UNSAT, skip + return None + # Negate sum(ci*xi) <= bound as sum(-ci*xi) <= -(bound+1) + neg_coeffs = [-c for c, _ in linear.terms] + fnames = [_fzn_var(v, var_map) for _, v in linear.terms] + coeffs_str = "[" + ", ".join(str(c) for c in neg_coeffs) + "]" + vars_str = "[" + ", ".join(fnames) + "]" + rhs = -(linear.bound + 1) + return f"constraint int_lin_le({coeffs_str}, {vars_str}, {rhs});" + + +def verify_axiom( + step: HLStep, + fzn_content: str, + var_map: dict, + solver_bin: str, + timeout: int = 30, +) -> bool: + """Verify that M ∧ H ∧ ¬R is UNSAT using Pumpkin. + + Returns True if UNSAT (axiom verified). + Returns False if SAT or unknown (axiom not verified). + """ + neg_lin = _fzn_negated_linear(step.linear, var_map) + if neg_lin is None: + # Negation is false; M ∧ H ∧ false is trivially UNSAT. + return True + + extra = [_fzn_predicate(pred, var_map) for pred in step.predicates] + if neg_lin: + extra.append(neg_lin) + + # Insert new constraints before the solve statement. + solve_idx = fzn_content.rfind("\nsolve ") + if solve_idx == -1: + augmented = fzn_content + "\n" + "\n".join(extra) + "\n" + else: + augmented = ( + fzn_content[: solve_idx + 1] + + "\n".join(extra) + + "\n" + + fzn_content[solve_idx + 1 :] + ) + + with tempfile.NamedTemporaryFile(suffix=".fzn", mode="w", delete=False) as tmp: + tmp.write(augmented) + tmp_path = tmp.name + + try: + result = subprocess.run( + [solver_bin, tmp_path], + capture_output=True, + text=True, + timeout=timeout, + ) + return "=====UNSATISFIABLE=====" in result.stdout + except subprocess.TimeoutExpired: + print( + f" TIMEOUT: axiom at line {step.line_num}: {_hl_str(step)}", + file=sys.stderr, + ) + return False + finally: + os.unlink(tmp_path) + + +def run_axiom_verification( + proof_path: str, + var_map: dict, + fzn_path: str, + solver_bin: str, +) -> bool: + """Verify all 'i' axioms against the FlatZinc model. + + Streams through the proof file; only one step is held in memory at a time. + Returns True if all axioms are verified, False otherwise. + """ + with open(fzn_path) as f: + fzn_content = f.read() + + n_ok = n_fail = n_skip = 0 + + for step in iter_steps(proof_path): + if step.kind != "i": + continue + + neg_lin = _fzn_negated_linear(step.linear, var_map) + if neg_lin is None: + # Trivially UNSAT — the negation is false, no solver call needed. + n_skip += 1 + continue + + ok = verify_axiom(step, fzn_content, var_map, solver_bin) + if ok: + n_ok += 1 + print(f"OK: axiom at line {step.line_num}") + else: + n_fail += 1 + print( + f"FAIL: axiom at line {step.line_num}: {_hl_str(step)}", + file=sys.stderr, + ) + + print( + f"Axiom verification: {n_ok} ok, {n_fail} failed, {n_skip} skipped", + file=sys.stderr, + ) + return n_fail == 0 + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def main(): + parser = argparse.ArgumentParser( + description="HL proof checker: Z3 derivation checks and optional axiom verification." + ) + parser.add_argument("proof", help="Proof file (.hl)") + parser.add_argument( + "--fzn", + metavar="FILE", + help="FlatZinc model to verify axioms against (enables axiom verification)", + ) + parser.add_argument( + "--solver", + default="pumpkin-solver", + metavar="BIN", + help="Pumpkin solver binary used for axiom verification (default: pumpkin-solver)", + ) + args = parser.parse_args() + + ok = run_z3_checks(args.proof) + if not ok: + sys.exit(1) + + if args.fzn: + var_map = _collect_var_map(args.proof) + ok = run_axiom_verification(args.proof, var_map, args.fzn, args.solver) + if not ok: + sys.exit(2) + + +if __name__ == "__main__": + main() diff --git a/hl-scripts/main.py b/hl-scripts/main.py new file mode 100644 index 000000000..1e8d82f40 --- /dev/null +++ b/hl-scripts/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from hl-scripts!") + + +if __name__ == "__main__": + main() diff --git a/hl-scripts/plot.png b/hl-scripts/plot.png new file mode 100644 index 000000000..649cb567b Binary files /dev/null and b/hl-scripts/plot.png differ diff --git a/hl-scripts/plot_hl_trace.py b/hl-scripts/plot_hl_trace.py new file mode 100644 index 000000000..b83ce28f2 --- /dev/null +++ b/hl-scripts/plot_hl_trace.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +""" +Plot the relationship between backtrack level and number of linear terms +in derived hypercube linear constraints from a Pumpkin HL trace file. + +Usage: python plot_hl_trace.py [--output ] +""" + +import argparse +import re +import sys +from collections import defaultdict + +import matplotlib.pyplot as plt +import numpy as np + +# d -> ... <= confl@ bt= +D_LINE_RE = re.compile( + r'^d \d+ .*?-> (.*?) confl@(\d+) bt=(\d+)\s*$' +) + + +def count_linear_terms(linear_part: str) -> int: + """Count coefficient-variable pairs before the '<=' in the linear part.""" + # linear_part is everything between '->' and 'confl@...' + # Format: [coeff var]* <= rhs + le_idx = linear_part.rfind('<=') + if le_idx == -1: + return 0 + tokens = linear_part[:le_idx].split() + # Each term is two tokens: coefficient and variable name + return len(tokens) // 2 + + +def parse_trace(path: str): + """Stream-parse the trace and yield (confl_level, bt_level, num_terms) per 'd' line.""" + with open(path, 'r') as f: + for line in f: + if not line.startswith('d '): + continue + m = D_LINE_RE.match(line) + if not m: + continue + linear_part = m.group(1) + confl = int(m.group(2)) + bt = int(m.group(3)) + n_terms = count_linear_terms(linear_part) + yield confl, bt, n_terms + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('trace', help='Path to the .hl trace file') + parser.add_argument('--output', '-o', default=None, + help='Save plot to this path instead of displaying it') + args = parser.parse_args() + + bt_levels = [] + n_terms_list = [] + confl_levels = [] + + for confl, bt, n_terms in parse_trace(args.trace): + bt_levels.append(bt) + n_terms_list.append(n_terms) + confl_levels.append(confl) + + if not bt_levels: + print("No 'd' lines found in trace.", file=sys.stderr) + sys.exit(1) + + bt_levels = np.array(bt_levels) + n_terms_list = np.array(n_terms_list) + confl_levels = np.array(confl_levels) + + print(f"Parsed {len(bt_levels)} derived HLs") + print(f" bt range: [{bt_levels.min()}, {bt_levels.max()}]") + print(f" terms range: [{n_terms_list.min()}, {n_terms_list.max()}]") + + # --- Aggregate: median and percentiles of #terms per bt level --- + bt_unique = np.unique(bt_levels) + medians = [] + p25 = [] + p75 = [] + means = [] + for bt in bt_unique: + mask = bt_levels == bt + vals = n_terms_list[mask] + medians.append(np.median(vals)) + p25.append(np.percentile(vals, 25)) + p75.append(np.percentile(vals, 75)) + means.append(np.mean(vals)) + + medians = np.array(medians) + p25 = np.array(p25) + p75 = np.array(p75) + + # --- Correlation --- + corr = np.corrcoef(bt_levels, n_terms_list)[0, 1] + print(f" Pearson r(bt, #terms) = {corr:.3f}") + + fig, axes = plt.subplots(2, 1, figsize=(10, 10)) + fig.suptitle('HL Conflict Analysis: Backtrack Level vs. Linear Term Count', fontsize=13) + + # Panel 1: scatter plot (raw data) + ax = axes[0] + ax.scatter(bt_levels, n_terms_list, alpha=0.15, s=6, color='steelblue', label='derived HL') + ax.set_xlabel('Backtrack level (bt)') + ax.set_ylabel('Number of linear terms') + ax.set_title(f'Raw data (n={len(bt_levels)}, Pearson r={corr:.3f})') + ax.legend(loc='upper left', markerscale=3) + + # Panel 2: aggregated — bar chart of median per bt level, with IQR error bars + ax = axes[1] + yerr_low = medians - p25 + yerr_high = p75 - medians + ax.bar(bt_unique, medians, color='steelblue', alpha=0.7, label='median', + yerr=[yerr_low, yerr_high], error_kw=dict(ecolor='black', capsize=3, linewidth=0.8)) + ax.set_xlabel('Backtrack level (bt)') + ax.set_ylabel('Number of linear terms') + ax.set_title('Median #terms per backtrack level (error bars = IQR)') + ax.legend() + + plt.tight_layout() + + if args.output: + plt.savefig(args.output, dpi=150) + print(f"Plot saved to {args.output}") + else: + plt.show() + + +if __name__ == '__main__': + main() diff --git a/hl-scripts/pyproject.toml b/hl-scripts/pyproject.toml new file mode 100644 index 000000000..5baea143e --- /dev/null +++ b/hl-scripts/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "hl-scripts" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "matplotlib>=3.10.8", + "z3-solver>=4.16.0.0", +] diff --git a/hl-scripts/uv.lock b/hl-scripts/uv.lock new file mode 100644 index 000000000..dbdf98911 --- /dev/null +++ b/hl-scripts/uv.lock @@ -0,0 +1,391 @@ +version = 1 +revision = 1 +requires-python = ">=3.13" + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257 }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034 }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672 }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234 }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169 }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859 }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062 }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932 }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024 }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578 }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524 }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730 }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897 }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751 }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486 }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106 }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548 }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297 }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023 }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157 }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570 }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713 }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189 }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251 }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810 }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871 }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264 }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819 }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650 }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833 }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692 }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424 }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300 }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769 }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892 }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748 }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554 }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118 }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555 }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295 }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027 }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428 }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331 }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831 }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, +] + +[[package]] +name = "fonttools" +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155 }, + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802 }, + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926 }, + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575 }, + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693 }, + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920 }, + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928 }, + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514 }, + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442 }, + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901 }, + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608 }, + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726 }, + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422 }, + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979 }, + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733 }, + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663 }, + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288 }, + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023 }, + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599 }, + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933 }, + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232 }, + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987 }, + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021 }, + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147 }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647 }, +] + +[[package]] +name = "hl-scripts" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "matplotlib" }, + { name = "z3-solver" }, +] + +[package.metadata] +requires-dist = [ + { name = "matplotlib", specifier = ">=3.10.8" }, + { name = "z3-solver", specifier = ">=4.16.0.0" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166 }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395 }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065 }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903 }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751 }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793 }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041 }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292 }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865 }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369 }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989 }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645 }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237 }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573 }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998 }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700 }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537 }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514 }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848 }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542 }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447 }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918 }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856 }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580 }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018 }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804 }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482 }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328 }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410 }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231 }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489 }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063 }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913 }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782 }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815 }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925 }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322 }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857 }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376 }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549 }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680 }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905 }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086 }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577 }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794 }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646 }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511 }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858 }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539 }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310 }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244 }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154 }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377 }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288 }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158 }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260 }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403 }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687 }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032 }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076 }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794 }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474 }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637 }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678 }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686 }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917 }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679 }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336 }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653 }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356 }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000 }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043 }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075 }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481 }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473 }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896 }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193 }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444 }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719 }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205 }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785 }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361 }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357 }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610 }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011 }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801 }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560 }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933 }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532 }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661 }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539 }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806 }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682 }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810 }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394 }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556 }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311 }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060 }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302 }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407 }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631 }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691 }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241 }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767 }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169 }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477 }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487 }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002 }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353 }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914 }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005 }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974 }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591 }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700 }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781 }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959 }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768 }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181 }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035 }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958 }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020 }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758 }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948 }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325 }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883 }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474 }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500 }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755 }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643 }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831 }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837 }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528 }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401 }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094 }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402 }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005 }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669 }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194 }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423 }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667 }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580 }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896 }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266 }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508 }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927 }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624 }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252 }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550 }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114 }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667 }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966 }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241 }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592 }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542 }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765 }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848 }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515 }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159 }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185 }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386 }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384 }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599 }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021 }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360 }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628 }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321 }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723 }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400 }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835 }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225 }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541 }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251 }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807 }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935 }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720 }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498 }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413 }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084 }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579 }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "z3-solver" +version = "4.16.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/93/3b/2b714c40ef2ecf6d8aa080056b9c24a77fe4ca2c83abd83e9c93d34212ac/z3_solver-4.16.0.0.tar.gz", hash = "sha256:263d9ad668966e832c2b246ba0389298a599637793da2dc01cc5e4ef4b0b6c78", size = 5098891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/5d/9b277a80333db6b85fedd0f5082e311efcbaec47f2c44c57d38953c2d4d9/z3_solver-4.16.0.0-py3-none-macosx_15_0_arm64.whl", hash = "sha256:cc52843cfdd3d3f2cd24bedc62e71c18af8c8b7b23fb05e639ab60b01b5f8f2f", size = 36963251 }, + { url = "https://files.pythonhosted.org/packages/1c/c4/fc99aa544930fb7bfcd88947c2788f318acaf1b9704a7a914445e204436a/z3_solver-4.16.0.0-py3-none-macosx_15_0_x86_64.whl", hash = "sha256:e292df40951523e4ecfbc8dee549d93dee00a3fe4ee4833270d19876b713e210", size = 47523873 }, + { url = "https://files.pythonhosted.org/packages/f6/e6/98741b086b6e01630a55db1fbda596949f738204aac14ef35e64a9526ccb/z3_solver-4.16.0.0-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:afae2551f795670f0522cfce82132d129c408a2694adff71eb01ba0f2ece44f9", size = 31741807 }, + { url = "https://files.pythonhosted.org/packages/e7/2e/295d467c7c796c01337bff790dbedc28cf279f9d365ed64aa9f8ca6b2ba1/z3_solver-4.16.0.0-py3-none-manylinux_2_38_aarch64.whl", hash = "sha256:358648c3b5ef82b9ec9a25711cf4fc498c7881f03a9f4a2ea6ffa9304ca65d94", size = 27326531 }, + { url = "https://files.pythonhosted.org/packages/34/df/29816ce4de24cca3acb007412f9c6fba603e55fcc27ce8c2aade0939057a/z3_solver-4.16.0.0-py3-none-win32.whl", hash = "sha256:cc64c4d41fbebe419fccddb044979c3d95b41214547db65eecdaa67fafef7fe0", size = 13341643 }, + { url = "https://files.pythonhosted.org/packages/86/20/cef4f4d70845df24572d005d19995f92b7f527eb2ffb63a3f5f938a0de2e/z3_solver-4.16.0.0-py3-none-win_amd64.whl", hash = "sha256:eb5df383cb6a3d6b7767dbdca348ac71f6f41e82f76c9ac42002a1f55e35f462", size = 16419861 }, + { url = "https://files.pythonhosted.org/packages/e1/18/7dc1051093abfd6db56ce9addb63c624bfa31946ccb9cfc9be5e75237a26/z3_solver-4.16.0.0-py3-none-win_arm64.whl", hash = "sha256:28729eae2c89112e37697acce4d4517f5e44c6c54d36fed9cf914b06f380cbd6", size = 15084866 }, +] diff --git a/minizinc/lib-for-hypercubes/CHANGELOG.txt b/minizinc/lib-for-hypercubes/CHANGELOG.txt new file mode 100644 index 000000000..8f53e03fd --- /dev/null +++ b/minizinc/lib-for-hypercubes/CHANGELOG.txt @@ -0,0 +1,6 @@ + +2015-07-20 + Special handling of array_int_element which is a linear fn. No much diff on ProjPlanning_12_8. + [REVERTED] + Replaced array_int_element by the old (forall(.. -> ..)) + 82s vs 45s on ProjPlanning_12_8 \ No newline at end of file diff --git a/minizinc/lib-for-hypercubes/domain_encodings.mzn b/minizinc/lib-for-hypercubes/domain_encodings.mzn new file mode 100644 index 000000000..43479eaa8 --- /dev/null +++ b/minizinc/lib-for-hypercubes/domain_encodings.mzn @@ -0,0 +1,109 @@ +/*%-----------------------------------------------------------------------------% +% Domain encodings +%-----------------------------------------------------------------------------% +*/ +% Linear equality encoding + +% Single variable: x = d <-> x_eq_d[d] +predicate equality_encoding(var int: x, array [int] of var int: x_eq_d) = + x in index_set(x_eq_d) /\ + sum (d in dom(x)) (x_eq_d[d]) = 1 /\ + sum (d in dom(x)) (d * x_eq_d[d]) = x /\ + % my_trace( "eq_enc: \(x), index_set(pp)=" ++ show(index_set( x_eq_d )) ++ "\n" ) /\ + if fPostprocessDomains then equality_encoding__POST(x, x_eq_d) else true endif; + +% Two variables: x = d /\ y = e <-> x_eq_d[d] /\ y_eq_e[e] /\ xy_eq_de[d, e] +predicate equality_encoding( + var int: x, + var int: y, + array [int] of var int: x_eq_d, + array [int] of var int: y_eq_e, + array [int, int] of var int: xy_eq_de, +) = + x in index_set(x_eq_d) /\ + y in index_set(y_eq_e) /\ + index_set(x_eq_d) == index_set_1of2(xy_eq_de) /\ + index_set(y_eq_e) == index_set_2of2(xy_eq_de) /\ + sum (d in dom(x), e in dom(y)) (xy_eq_de[d, e]) = 1 /\ + forall (d in dom(x)) (sum (e in dom(y)) (xy_eq_de[d, e]) = x_eq_d[d]) /\ + forall (e in dom(y)) (sum (d in dom(x)) (xy_eq_de[d, e]) = y_eq_e[e]); + +% Array of variables: x[i] = d <-> x_eq_d[i,d] +predicate equality_encoding(array [int] of var int: x, array [int, int] of var int: x_eq_d) = + forall (i in index_set(x)) ( + x[i] in index_set_2of2(x_eq_d) /\ + sum (d in index_set_2of2(x_eq_d)) (x_eq_d[i, d]) = 1 /\ + sum (d in index_set_2of2(x_eq_d)) (d * x_eq_d[i, d]) = x[i] + ); + +function var int: eq_new_var(var int: x, int: i) :: promise_total = + if i in dom(x) then let { var 0..1: xi } in xi else 0 endif; + +function array [int] of var int: eq_encode(var int: x) :: promise_total = + let { + array [int] of var int: y = array1d(lb(x)..ub(x), [eq_new_var(x, i) | i in lb(x)..ub(x)]); + constraint equality_encoding(x, y); + } in % constraint + % if card(dom(x))>0 then + % my_trace(" eq_encode: dom(\(x)) = " ++ show(dom(x)) ++ ", card( dom(\(x)) ) = " ++ show(card(dom(x))) ++ "\n") + % else true endif; + %% constraint assert(card(dom(x))>1, " eq_encode: card(dom(\(x))) == " ++ show(card(dom(x)))); + y; + +function array [int] of int: eq_encode(int: x) :: promise_total = + array1d(lb(x)..ub(x), [if i = x then 1 else 0 endif | i in lb(x)..ub(x)]); + +%%% The same for 2 variables: +function var int: eq_new_var(var int: x, int: i, var int: y, int: j) :: promise_total = + if i in dom(x) /\ j in dom(y) then let { var 0..1: xi } in xi else 0 endif; + +function array [int, int] of var int: eq_encode(var int: x, var int: y) :: promise_total = + let { + array [int] of var int: pX = eq_encode(x); + array [int] of var int: pY = eq_encode(y); + array [int, int] of var int: pp = + array2d( + index_set(pX), + index_set(pY), + [eq_new_var(x, i, y, j) | i in index_set(pX), j in index_set(pY)], + ); + constraint equality_encoding(x, y, pX, pY, pp); + } in pp; + +function array [int, int] of int: eq_encode(int: x, int: y) :: promise_total = + % let { + % constraint if card(dom(x))*card(dom(y))>200 then + % my_trace(" eq_encode: dom(\(x)) = " ++ show(dom(x)) ++ ", dom(\(y)) = " ++ show(dom(y)) ++ "\n") + % else true endif; + % } in + array2d( + lb(x)..ub(x), + lb(y)..ub(y), + [if i == x /\ j == y then 1 else 0 endif | i in lb(x)..ub(x), j in lb(y)..ub(y)], + ); + +function array [int, int] of var int: eq_encode(array [int] of var int: x) :: promise_total = + let { + array [index_set(x), lb_array(x)..ub_array(x)] of var int: y = + array2d( + index_set(x), + lb_array(x)..ub_array(x), + [ + let { + array [int] of var int: xi = eq_encode(x[i]); + } in if j in index_set(xi) then xi[j] else 0 endif | + i in index_set(x), + j in lb_array(x)..ub_array(x), + ], + ); + } in y; + +function array [int, int] of int: eq_encode(array [int] of int: x) :: promise_total = + array2d( + index_set(x), + lb_array(x)..ub_array(x), + [if j = x[i] then 1 else 0 endif | i in index_set(x), j in lb_array(x)..ub_array(x)], + ); + +%-----------------------------------------------------------------------------% +%-----------------------------------------------------------------------------% diff --git a/minizinc/lib-for-hypercubes/fzn_all_different_int.mzn b/minizinc/lib-for-hypercubes/fzn_all_different_int.mzn new file mode 100644 index 000000000..bd3518fd9 --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_all_different_int.mzn @@ -0,0 +1,23 @@ +%-----------------------------------------------------------------------------% +% 'all_different' constrains an array of objects to be all different. +% +% Linear version: equality encoding; see e.g. [Refalo, CP 2000] +% +% For a given d in dom(x), at most one i with x_i = d can exist. +%-----------------------------------------------------------------------------% + +include "domain_encodings.mzn"; + +predicate fzn_all_different_int(array [int] of var int: x) = + if length(x) <= 1 then + true + else + let { + array [int, int] of var 0..1: x_eq_d = eq_encode(x); + } in ( + % my_trace(" all_different_int: x[" ++ show(index_set(x)) ++ "]\n") /\ + forall (d in index_set_2of2(x_eq_d)) (sum (i in index_set_1of2(x_eq_d)) (x_eq_d[i, d]) <= 1) + ) + endif; + +%-----------------------------------------------------------------------------% diff --git a/minizinc/lib-for-hypercubes/fzn_alldifferent_except_0.mzn b/minizinc/lib-for-hypercubes/fzn_alldifferent_except_0.mzn new file mode 100644 index 000000000..978f30ac9 --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_alldifferent_except_0.mzn @@ -0,0 +1,20 @@ +/** @group globals.alldifferent + Constrain the array of integers \a vs to be all different except those + elements that are assigned the value 0. +*/ +predicate fzn_alldifferent_except_0(array [int] of var int: vs) = + % forall(i, j in index_set(vs) where i < j) ( + % (vs[i] != 0 /\ vs[j] != 0) -> vs[i] != vs[j] + % ); + if length(vs) <= 1 then + true + else + let { + array [int, int] of var 0..1: x_eq_d = eq_encode(vs); + } in ( + % my_trace(" alldifferent_except_0: x[" ++ show(index_set(vs)) ++ "]\n") /\ + forall (d in index_set_2of2(x_eq_d) diff {0}) ( + sum (i in index_set_1of2(x_eq_d)) (x_eq_d[i, d]) <= 1 + ) + ) + endif; diff --git a/minizinc/lib-for-hypercubes/fzn_circuit.mzn b/minizinc/lib-for-hypercubes/fzn_circuit.mzn new file mode 100644 index 000000000..ab90c7b5c --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_circuit.mzn @@ -0,0 +1,47 @@ +include "alldifferent.mzn"; + +% Linear version. +predicate fzn_circuit(array [int] of var int: x) = + if length(x) = 0 then + true + else + let { + set of int: S = index_set(x); + int: l = min(S); + int: u = max(S); + int: n = card(S); + constraint forall (i in S) (x[i] in S diff {i}); %% Self-mapping and exclude i->i before alldifferent + } in alldifferent(x) /\ + % alldifferent(order) /\ + if nMZN__fSECcuts > 0 then + let { + array [int, int] of var int: eq_x = eq_encode(x); + constraint assert(l == min(index_set_2of2(eq_x)), "circuit: index set mismatch"); %% self-mapping + constraint assert(u == max(index_set_2of2(eq_x)), "circuit: index set mismatch"); + } in circuit__SECcuts(array1d(eq_x)) + else + true + endif /\ + if nMZN__fSECcuts < 2 then + %%% MTZ model. Note that INTEGER order vars seem better!: + let { + array [l + 1..l + n - 1] of var 2..n: order; + } in forall (i, j in l + 1..l + n - 1 where i != j /\ j in dom(x[i])) ( + order[i] - order[j] + (n - 1) * bool2int(x[i] == j) + (n - 3) * bool2int(x[j] == i) <= %% the Desrochers & Laporte '91 term + %%%% --- strangely enough it is much worse on vrp-s2-v2-c7_vrp-v2-c7_det_ADAPT_1_INVERSE.mzn! + n - 2 + ) + else + true + endif /\ + %% ... but seems improved with this (leaving also for SEC) + if n > 2 then + forall (i, j in S where i < j) (x[i] != j \/ x[j] != i) %% Only this improves overall with DL'91 + else + true + endif + endif; + +%-----------------------------------------------------------------------------% +predicate circuit__SECcuts(array [int] of var int: x); %% passed on +%-----------------------------------------------------------------------------% diff --git a/minizinc/lib-for-hypercubes/fzn_cumulative.mzn b/minizinc/lib-for-hypercubes/fzn_cumulative.mzn new file mode 100644 index 000000000..c6518c61a --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_cumulative.mzn @@ -0,0 +1,111 @@ +/** @group globals.scheduling + Requires that a set of tasks given by start times \a s, durations \a d, and + resource requirements \a r, never require more than a global resource bound + \a b at any one time. + + Assumptions: + - forall \p i, \a d[\p i] >= 0 and \a r[\p i] >= 0 + + Linear version. +*/ + +predicate fzn_cumulative( + array [int] of var int: s, + array [int] of var int: d, + array [int] of var int: r, + var int: b :: promise_ctx_monotone, +) = + if mzn_in_redundant_constraint() /\ fMZN__IgnoreRedundantCumulative then + true + else + let { + set of int: tasks = {i | i in index_set(s) where ub(r[i]) > 0 /\ ub(d[i]) > 0}; + set of int: times = dom_array([s[i] | i in tasks]); + } in if 0 == card(tasks) then + /*true*/ + 0 == card(index_set(s)) \/ b >= 0 + elseif MZN__Cumulative_Fixed_d_r /\ is_fixed(d) /\ is_fixed(r) /\ is_fixed(b) then + fzn_cumulative_fixed_d_r(s, fix(d), fix(r), fix(b)) + elseif nMZN__UnarySizeMax_cumul >= card(times) * card(tasks) then + cumulative_time_decomp(s, d, r, b, times) + else + cumulative_task_decomp(s, d, r, b) + endif + endif; + +%% Can be called with a given set of times: +predicate cumulative_set_times( + array [int] of var int: s, + array [int] of var int: d, + array [int] of var int: r, + var int: b, + set of int: TIMES01, +) = + assert( + index_set(s) == index_set(d) /\ index_set(s) == index_set(r), + "cumulative: the 3 array arguments must have identical index sets", + assert( + lb_array(d) >= 0 /\ lb_array(r) >= 0, + "cumulative: durations and resource usages must be non-negative", + let { + set of int: tasks = {i | i in index_set(s) where ub(r[i]) > 0 /\ ub(d[i]) > 0}; + set of int: times = dom_array([s[i] | i in tasks]) intersect TIMES01; + } in if false then + cumulative_time_decomp(s, d, r, b, times) + else + cumulative_task_decomp(s, d, r, b) + endif, + ), + ); + +predicate cumulative_time_decomp( + array [int] of var int: s, + array [int] of var int: d, + array [int] of var int: r, + var int: b :: promise_ctx_monotone, + set of int: TIMES01, +) = + let { + set of int: tasks = {i | i in index_set(s) where ub(r[i]) > 0 /\ ub(d[i]) > 0}; + set of int: times = { + i | + i in min([lb(s[i]) | i in tasks])..max([ub(s[i]) + ub(d[i]) | i in tasks]) + where i in TIMES01, + }; + } in forall (t in times) ( + b >= + sum (i in tasks) ( + if is_fixed(d[i]) then + bool2int(s[i] in t - fix(d[i]) + 1..t) + else + bool2int(s[i] <= t /\ t < s[i] + d[i]) + endif * + r[i] + ) + ); + +predicate cumulative_task_decomp( + array [int] of var int: s, + array [int] of var int: d, + array [int] of var int: r, + var int: b :: promise_ctx_monotone, +) = + let { + set of int: tasks = {i | i in index_set(s) where ub(r[i]) > 0 /\ ub(d[i]) > 0}; + } in forall (j in tasks) ( + b - r[j] >= + sum ( + i in tasks where i != j /\ lb(s[i]) <= ub(s[j]) /\ lb(s[j]) < ub(s[i] + d[i]), %% -- seems slower on mspsp ??? + ) ( + r[i] * %% r[i] * ! + bool2int(s[i] <= s[j] /\ s[j] < s[i] + d[i]) + ) + ); + +%% A global cumulative with SCIP: fixed d and r +predicate fzn_cumulative_fixed_d_r( + array [int] of var int: s, + array [int] of int: d, + array [int] of int: r, + int: b, +); diff --git a/minizinc/lib-for-hypercubes/fzn_if_then_else_float.mzn b/minizinc/lib-for-hypercubes/fzn_if_then_else_float.mzn new file mode 100644 index 000000000..f4e2e0bfe --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_if_then_else_float.mzn @@ -0,0 +1,10 @@ +predicate fzn_if_then_else_float( + array [int] of var bool: c, + array [int] of float: x, + var float: y, +) = + let { + array [index_set(c)] of var 0..1: d; + } in forall (i in index_set(c)) (sum (j in 1..i - 1) (c[j]) + d[i] >= c[i]) /\ + sum(d) = 1 /\ + y = sum (i in index_set(c)) (d[i] * x[i]); diff --git a/minizinc/lib-for-hypercubes/fzn_if_then_else_int.mzn b/minizinc/lib-for-hypercubes/fzn_if_then_else_int.mzn new file mode 100644 index 000000000..851f12163 --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_if_then_else_int.mzn @@ -0,0 +1,6 @@ +predicate fzn_if_then_else_int(array [int] of var bool: c, array [int] of int: x, var int: y) = + let { + array [index_set(c)] of var 0..1: d; + } in forall (i in index_set(c)) (sum (j in 1..i - 1) (c[j]) + d[i] >= c[i]) /\ + sum(d) = 1 /\ + y = sum (i in index_set(c)) (d[i] * x[i]); diff --git a/minizinc/lib-for-hypercubes/fzn_inverse.mzn b/minizinc/lib-for-hypercubes/fzn_inverse.mzn new file mode 100644 index 000000000..c61833a12 --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_inverse.mzn @@ -0,0 +1,13 @@ +%-----------------------------------------------------------------------------% +% Constrains two arrays of int variables to represent inverse functions. +% All the values in each array must be within the index set of the other array. +% +% Linear version. +%-----------------------------------------------------------------------------% + +include "fzn_inverse_in_range.mzn"; + +predicate fzn_inverse(array [int] of var int: f, array [int] of var int: invf) = + forall (i in index_set(f)) (f[i] in index_set(invf)) /\ + forall (j in index_set(invf)) (invf[j] in index_set(f)) /\ + fzn_inverse_in_range(f, invf); diff --git a/minizinc/lib-for-hypercubes/fzn_inverse_in_range.mzn b/minizinc/lib-for-hypercubes/fzn_inverse_in_range.mzn new file mode 100644 index 000000000..08763f9c2 --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_inverse_in_range.mzn @@ -0,0 +1,2 @@ +predicate fzn_inverse_in_range(array [int] of var int: f, array [int] of var int: invf) = + forall (i in index_set(f), j in index_set(invf)) ((j == f[i] <-> i == invf[j])); diff --git a/minizinc/lib-for-hypercubes/fzn_inverse_in_range_reif.mzn b/minizinc/lib-for-hypercubes/fzn_inverse_in_range_reif.mzn new file mode 100644 index 000000000..b48ca06be --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_inverse_in_range_reif.mzn @@ -0,0 +1,5 @@ +predicate fzn_inverse_in_range_reif( + array [int] of var int: f, + array [int] of var int: invf, + var bool: b, +) = b <-> forall (i in index_set(f), j in index_set(invf)) ((j == f[i] <-> i == invf[j])); diff --git a/minizinc/lib-for-hypercubes/fzn_inverse_reif.mzn b/minizinc/lib-for-hypercubes/fzn_inverse_reif.mzn new file mode 100644 index 000000000..5478fa164 --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_inverse_reif.mzn @@ -0,0 +1,5 @@ +predicate fzn_inverse_reif(array [int] of var int: f, array [int] of var int: invf, var bool: b) = + b <-> + forall (i in index_set(f), j in index_set(invf)) ( + f[i] in index_set(invf) /\ invf[j] in index_set(f) /\ (j == f[i] <-> i == invf[j]) + ); diff --git a/minizinc/lib-for-hypercubes/fzn_lex_chain_lesseq_bool.mzn b/minizinc/lib-for-hypercubes/fzn_lex_chain_lesseq_bool.mzn new file mode 100644 index 000000000..e7dbb2a25 --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_lex_chain_lesseq_bool.mzn @@ -0,0 +1,3 @@ +include "fzn_lex_chain_lesseq_int.mzn"; + +predicate fzn_lex_chain_lesseq_bool(array [int, int] of var bool: a) = fzn_lex_chain_lesseq_int(a); diff --git a/minizinc/lib-for-hypercubes/fzn_lex_chain_lesseq_int.mzn b/minizinc/lib-for-hypercubes/fzn_lex_chain_lesseq_int.mzn new file mode 100644 index 000000000..1c5f93d9e --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_lex_chain_lesseq_int.mzn @@ -0,0 +1,20 @@ +include "lex_lesseq.mzn"; +include "fzn_lex_chain_lesseq_orbitope.mzn"; + +predicate fzn_lex_chain_lesseq_int(array [int, int] of var int: a) = + if MZN__Orbitope /\ dom_array(a) subset 0..1 then + fzn_lex_chain_lesseq__orbitope( + array1d(a), + card(index_set_1of2(a)), + 0, + true, + not mzn_in_symmetry_breaking_constraint(), + ) + else + fzn_lex_chain_lesseq_int__CP(a) + endif; + +predicate fzn_lex_chain_lesseq_int__CP(array [int, int] of var int: a) = + let { + set of int: is2 = index_set_2of2(a); + } in (forall (j in is2 where j + 1 in is2) (lex_lesseq(col(a, j), col(a, j + 1)))); diff --git a/minizinc/lib-for-hypercubes/fzn_lex_chain_lesseq_orbitope.mzn b/minizinc/lib-for-hypercubes/fzn_lex_chain_lesseq_orbitope.mzn new file mode 100644 index 000000000..c6d6e37ec --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_lex_chain_lesseq_orbitope.mzn @@ -0,0 +1,37 @@ +include "lex_lesseq.mzn"; + +predicate fzn_lex_chain_lesseq_orbitope(array [int, int] of var int: a, int: kind) = + if MZN__Orbitope then + fzn_lex_chain_lesseq__orbitope( + array1d(a), + card(index_set_1of2(a)), + kind, + true, + not mzn_in_symmetry_breaking_constraint(), + ) + else + fzn_lex_chain_lesseq_orbitope__CP(a, kind) + endif; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% SCIP 7.0.2, binary matrix, columns sorted +predicate fzn_lex_chain_lesseq__orbitope( + array [int] of var int: matr, + int: m, + int: orbType, + bool: resolveprop, + bool: isModelCons, +); + +predicate fzn_lex_chain_lesseq_orbitope__CP(array [int, int] of var int: a, int: kind) = + let { + set of int: is2 = index_set_2of2(a); + } in ( + forall (j in is2 where j + 1 in is2) (lex_lesseq(col(a, j), col(a, j + 1))) /\ + if 1 == kind then + forall (i in index_set_1of2(a)) (1 == sum(row(a, i))) + elseif 2 == kind then + forall (i in index_set_1of2(a)) (1 >= sum(row(a, i))) + else + true + endif + ); diff --git a/minizinc/lib-for-hypercubes/fzn_lex_less_bool.mzn b/minizinc/lib-for-hypercubes/fzn_lex_less_bool.mzn new file mode 100644 index 000000000..c99fc2029 --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_lex_less_bool.mzn @@ -0,0 +1,11 @@ +%-----------------------------------------------------------------------------% +% Requires that the array 'x' is strictly lexicographically less than +% array 'y'. Compares them from first to last element, regardless of indices +%-----------------------------------------------------------------------------% + +include "fzn_lex_less_int.mzn"; + +predicate fzn_lex_less_bool(array [int] of var bool: x, array [int] of var bool: y) = + fzn_lex_less_int(x, y); + +%-----------------------------------------------------------------------------% diff --git a/minizinc/lib-for-hypercubes/fzn_lex_less_float.mzn b/minizinc/lib-for-hypercubes/fzn_lex_less_float.mzn new file mode 100644 index 000000000..87394adb0 --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_lex_less_float.mzn @@ -0,0 +1,33 @@ +%-----------------------------------------------------------------------------% +% Requires that the array 'x' is strictly lexicographically less than +% array 'y'. Compares them from first to last element, regardless of indices +%-----------------------------------------------------------------------------% + +predicate fzn_lex_less_float(array [int] of var float: x, array [int] of var float: y) = + assert(length(x) == length(y), "lex_less_float(\(x), \(y)): arrays of different lengths") /\ + fzn_lex_less_float__MIP(x, y); + +predicate fzn_lex_less_float__MIP(array [int] of var float: x, array [int] of var float: y) = + if length(y) = 0 then + false + elseif length(x) = 0 then + true + else + let { + int: lx = min(index_set(x)); + int: ux = max(index_set(x)); + int: ly = min(index_set(y)); + int: uy = max(index_set(y)); + int: size = min(ux - lx, uy - ly); + array [0..size] of var 0..1: fEQ; + array [0..size] of var 0..1: fLT; + } in sum(fLT) == 1 /\ + fEQ[0] + fLT[0] == 1 /\ + forall (i in 1..size) (fEQ[i - 1] == fEQ[i] + fLT[i]) /\ + forall (i in 0..size) ( + aux_float_eq_if_1(x[lx + i], y[ly + i], fEQ[i]) /\ + aux_float_lt_if_1(x[lx + i], y[ly + i], fLT[i]) + ) + endif; + +%-----------------------------------------------------------------------------% diff --git a/minizinc/lib-for-hypercubes/fzn_lex_less_int.mzn b/minizinc/lib-for-hypercubes/fzn_lex_less_int.mzn new file mode 100644 index 000000000..0fd89682a --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_lex_less_int.mzn @@ -0,0 +1,29 @@ +%-----------------------------------------------------------------------------% +% Requires that the array 'x' is strictly lexicographically less than array 'y'. +% Compares them from first to last element, regardless of indices +%-----------------------------------------------------------------------------% + +predicate fzn_lex_less_int(array [int] of var int: x, array [int] of var int: y) = + if length(y) = 0 then + false + elseif length(x) = 0 then + true + else + let { + int: lx = min(index_set(x)); + int: ux = max(index_set(x)); + int: ly = min(index_set(y)); + int: uy = max(index_set(y)); + int: size = min(ux - lx, uy - ly); + array [0..size] of var 0..1: fEQ; + array [0..size] of var 0..1: fLT; + } in sum(fLT) == 1 /\ + fEQ[0] + fLT[0] == 1 /\ + forall (i in 1..size) (fEQ[i - 1] == fEQ[i] + fLT[i]) /\ + forall (i in 0..size) ( + aux_int_eq_if_1(x[lx + i], y[ly + i], fEQ[i]) /\ + aux_int_lt_if_1(x[lx + i], y[ly + i], fLT[i]) + ) + endif; + +%-----------------------------------------------------------------------------% diff --git a/minizinc/lib-for-hypercubes/fzn_lex_lesseq_bool.mzn b/minizinc/lib-for-hypercubes/fzn_lex_lesseq_bool.mzn new file mode 100644 index 000000000..859747dce --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_lex_lesseq_bool.mzn @@ -0,0 +1,11 @@ +%-----------------------------------------------------------------------------% +% Requires that the array 'x' is lexicographically less than or equal to +% array 'y'. Compares them from first to last element, regardless of indices +%-----------------------------------------------------------------------------% + +include "fzn_lex_lesseq_int.mzn"; + +predicate fzn_lex_lesseq_bool(array [int] of var bool: x, array [int] of var bool: y) = + fzn_lex_lesseq_int(x, y); + +%-----------------------------------------------------------------------------% diff --git a/minizinc/lib-for-hypercubes/fzn_lex_lesseq_float.mzn b/minizinc/lib-for-hypercubes/fzn_lex_lesseq_float.mzn new file mode 100644 index 000000000..1ac84e816 --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_lex_lesseq_float.mzn @@ -0,0 +1,36 @@ +%-----------------------------------------------------------------------------% +% Requires that the array 'x' is lexicographically less than or equal to +% array 'y'. Compares them from first to last element, regardless of indices +%-----------------------------------------------------------------------------% + +predicate fzn_lex_lesseq_float(array [int] of var float: x, array [int] of var float: y) = + assert( + length(x) == length(y), %% SCIP cannot + "lex_lesseq_float(\(x), \(y)): arrays of different lengths", + ) /\ + fzn_lex_lesseq_float__MIP(x, y); + +predicate fzn_lex_lesseq_float__MIP(array [int] of var float: x, array [int] of var float: y) = + if length(x) = 0 then + true + elseif length(y) = 0 then + length(x) = 0 + else + let { + int: lx = min(index_set(x)); + int: ux = max(index_set(x)); + int: ly = min(index_set(y)); + int: uy = max(index_set(y)); + int: size = min(ux - lx, uy - ly); + array [0..size] of var 0..1: fEQ; + array [0..size] of var 0..1: fLT; + } in sum(fLT) <= 1 /\ + fEQ[0] + fLT[0] == 1 /\ + forall (i in 1..size) (fEQ[i - 1] == fEQ[i] + fLT[i]) /\ + forall (i in 0..size) ( + aux_float_eq_if_1(x[lx + i], y[ly + i], fEQ[i]) /\ + aux_float_lt_if_1(x[lx + i], y[ly + i], fLT[i]) + ) + endif; + +%-----------------------------------------------------------------------------% diff --git a/minizinc/lib-for-hypercubes/fzn_lex_lesseq_int.mzn b/minizinc/lib-for-hypercubes/fzn_lex_lesseq_int.mzn new file mode 100644 index 000000000..a3c0aa79c --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_lex_lesseq_int.mzn @@ -0,0 +1,63 @@ +%-----------------------------------------------------------------------------% +% Requires that the array 'x' is lexicographically less than or equal to +% array 'y'. Compares them from first to last element, regardless of indices +%-----------------------------------------------------------------------------% + +opt bool: UseCPLexLesseq; %% When not UseOrbisack, use CP decomposition +opt bool: OrbisackAlwaysModelConstraint; %% Use with SCIP 7.0.2 + +predicate fzn_lex_lesseq_int( + array [int] of var int: x :: promise_ctx_antitone, + array [int] of var int: y :: promise_ctx_monotone, +) = + assert( + length(x) == length(y), %% SCIP cannot + "lex_lesseq_int(\(x), \(y)): arrays of different lengths", + ) /\ + if MZN__Orbisack /\ dom_array(x) subset 0..1 /\ dom_array(y) subset 0..1 then + fzn_lex_lesseq__orbisack( + x, + y, + (occurs(OrbisackAlwaysModelConstraint) /\ deopt(OrbisackAlwaysModelConstraint)) \/ + not mzn_in_symmetry_breaking_constraint(), + ) + elseif occurs(UseCPLexLesseq) /\ deopt(UseCPLexLesseq) then + lex_lesseq_std_decomposition(x, y) + else + fzn_lex_lesseq_int__MIP(x, y) + endif; + +predicate fzn_lex_lesseq_int__MIP( + array [int] of var int: x :: promise_ctx_antitone, + array [int] of var int: y :: promise_ctx_monotone, +) = + if length(x) = 0 then + true + elseif length(y) = 0 then + length(x) = 0 + else + let { + int: lx = min(index_set(x)); + int: ux = max(index_set(x)); + int: ly = min(index_set(y)); + int: uy = max(index_set(y)); + int: size = min(ux - lx, uy - ly); + array [0..size] of var 0..1: fEQ; + array [0..size] of var 0..1: fLT; + } in sum(fLT) <= 1 /\ + fEQ[0] + fLT[0] == 1 /\ + forall (i in 1..size) (fEQ[i - 1] == fEQ[i] + fLT[i]) /\ + forall (i in 0..size) ( + aux_int_eq_if_1(x[lx + i], y[ly + i], fEQ[i]) /\ + aux_int_lt_if_1(x[lx + i], y[ly + i], fLT[i]) + ) + endif; + +%% SCIP constraint handler orbisack +predicate fzn_lex_lesseq__orbisack( + array [int] of var int: vec1, + array [int] of var int: vec2, + bool: isModelCons, +); + +%-----------------------------------------------------------------------------% diff --git a/minizinc/lib-for-hypercubes/fzn_regular.mzn b/minizinc/lib-for-hypercubes/fzn_regular.mzn new file mode 100644 index 000000000..8d0ebc5ed --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_regular.mzn @@ -0,0 +1,103 @@ +/** @group globals.extensional + The sequence of values in array \a x (which must all be in the range 1..\a S) + is accepted by the DFA of \a Q states with input 1..\a S and transition + function \a d (which maps (1..\a Q, 1..\a S) -> 0..\a Q)) and initial state \a q0 + (which must be in 1..\a Q) and accepting states \a F (which all must be in + 1..\a Q). We reserve state 0 to be an always failing state. +*/ +predicate fzn_regular( + array [int] of var int: x, + int: Q, + int: S, + array [int, int] of int: d, + int: q0, + set of int: F, +) = + if length(x) = 0 then + q0 in F + else + % my_trace(" regular: index_set(x)=" ++ show(index_set(x)) + % ++ ", dom_array(x)=" ++ show(dom_array(x)) + % ++ ", dom_array(a)=" ++ show(1..Q) + % ++ "\n") /\ + let { + % If x has index set m..n-1, then a[m] holds the initial state + % (q0), and a[i+1] holds the state we're in after processing + % x[i]. If a[n] is in F, then we succeed (ie. accept the string). + int: m = min(index_set(x)); + int: n = max(index_set(x)) + 1; + array [m..n] of var 1..Q: a; + constraint + a[m] = q0 /\ % Set a[0]. + a[n] in F; % Check the final state is in F. + constraint + forall ( + i in index_set(x), + ) ( + x[i] in 1..S /\ % Do this in case it's a var. + ( + %% trying to eliminate non-reachable states: + let { + set of int: va_R = {d[va, vx] | va in dom(a[i]), vx in dom(x[i])} diff {0}; %% Bug in MZN 2.0.4 + } in a[i + 1] in va_R + ) + ); + } in let { + constraint + forall (i in [n - i | i in 1..length(x)]) ( + a[i] in + {va | va in dom(a[i]) where exists (vx in dom(x[i])) (d[va, vx] in dom(a[i + 1]))} /\ + x[i] in + {vx | vx in dom(x[i]) where exists (va in dom(a[i])) (d[va, vx] in dom(a[i + 1]))} + ); + } in forall (i in index_set(x)) ( + let { + set of int: va_R = {d[va, vx] | va in dom(a[i]), vx in dom(x[i])} diff {0}; %% Bug in MZN 2.0.4 + } in % my_trace(" S" ++ show(i) + % ++ ": dom(a[i])=" ++ show(dom(a[i])) + % ++ ", va_R="++show(va_R) + % ++ ", index_set_2of2(eq_a) diff va_R=" ++ show(index_set_2of2(eq_a) diff va_R) + % ++ ", dom(a[i+1])=" ++ show(dom(a[i+1])) + % ) /\ + a[i + 1] in va_R + %/\ a[i+1] in min(va_R)..max(va_R) + ) /\ + ( + % /\ my_trace(" regular -- domains after prop: index_set(x)=" ++ show(index_set(x)) + % ++ ", dom_array(x)=" ++ show(dom_array(x)) + % ++ ", dom_array(a)=" ++ show(dom_array(a)) + % ++ "\n") + % /\ my_trace("\n") + let { + array [int, int] of var int: eq_a = eq_encode(a); + array [int, int] of var int: eq_x = eq_encode(x); + } in forall (i in index_set(x)) ( + % a[i+1] = d[a[i], x[i]] % Determine a[i+1]. + if card(dom(a[i])) * card(dom(x[i])) > nMZN__UnarySizeMax_1step_regular then + %% Implication decomposition: + forall (va in dom(a[i]), vx in dom(x[i])) ( + if d[va, vx] in dom(a[i + 1]) then + eq_a[i + 1, d[va, vx]] >= eq_a[i, va] + eq_x[i, vx] - 1 %% The only-if part of conj + else + 1 >= eq_a[i, va] + eq_x[i, vx] + endif + ) + else + %% Network-flow decomposition: + %% {regularIP07} M.-C. C{\^o}t{\'e}, B.~Gendron, and L.-M. Rousseau. + %% \newblock Modeling the regular constraint with integer programming. + let { + % array[int, int] of set of int: VX_a12 = %% set of x for given a1 that produce a2 + % array2d(1..S, 1..Q, [ { vx | vx in 1..S where d[va1, vx]==va2 } | va1 in dom(a[i]), va2 in dom(a[i+1]) ]); + array [int, int] of var int: ppAX = eq_encode(a[i], x[i]); + } in forall (va2 in dom(a[i + 1])) ( + eq_a[i + 1, va2] = + sum (va1 in dom(a[i]), vx in dom(x[i]) where d[va1, vx] == va2) (ppAX[va1, vx]) + ) /\ + forall (va1 in dom(a[i]), vx in dom(x[i])) ( + if not (d[va1, vx] in dom(a[i + 1])) then ppAX[va1, vx] == 0 else true endif + ) + endif + ) + ) + endif; diff --git a/minizinc/lib-for-hypercubes/fzn_sliding_sum.mzn b/minizinc/lib-for-hypercubes/fzn_sliding_sum.mzn new file mode 100644 index 000000000..43181ad89 --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_sliding_sum.mzn @@ -0,0 +1,9 @@ +predicate fzn_sliding_sum(int: low, int: up, int: seq, array [int] of var int: vs) = + let { + int: lx = min(index_set(vs)); + int: ux = max(index_set(vs)); + } in forall (i in lx..ux - seq + 1) ( + let { + var int: sum_of_l = sum (j in i..i + seq - 1) (vs[j]); + } in low <= sum_of_l /\ sum_of_l <= up + ); diff --git a/minizinc/lib-for-hypercubes/fzn_subcircuit.mzn b/minizinc/lib-for-hypercubes/fzn_subcircuit.mzn new file mode 100644 index 000000000..b8c45633f --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_subcircuit.mzn @@ -0,0 +1,62 @@ +include "alldifferent.mzn"; + +%% Linear version +predicate fzn_subcircuit(array [int] of var int: x) = + if length(x) = 0 then + true + else + let { + set of int: S = index_set(x); + int: l = min(S); + int: u = max(S); + int: n = card(S); + array [S] of var 1..n: order; + array [S] of var bool: ins = array1d(S, [x[i] != i | i in S]); + var l..u + 1: firstin = min([u + 1 + bool2int(ins[i]) * (i - u - 1) | i in S]); %% ... + var S: lastin; + var bool: empty = (firstin == u + 1); + } in alldifferent(x) /\ + ( + % NO alldifferent(order) /\ + % If the subcircuit is empty then each node points at itself. + % + empty <-> forall (i in S) (not ins[i]) + ) /\ + ( + % If the subcircuit is non-empty then order numbers the subcircuit. + % + (not empty) <-> + %% Another way to express minimum. + % forall(i in l..u+1)( + % i==firstin <-> ins[i] + % /\ forall(j in S where j firstin /\ + % The lastin node points at firstin. + x[lastin] = firstin /\ + % And both are in + ins[lastin] /\ + ins[firstin] /\ + % The successor of each node except where it is firstin is + % numbered one more than the predecessor. + % forall(i in S) ( + % (ins[i] /\ x[i] != firstin) -> order[x[i]] = order[i] + 1 + % ) /\ + %%% MTZ model. Note that INTEGER order vars seem better!: + forall (i, j in S where i != j) ( + order[i] - order[j] + n * bool2int(x[i] == j /\ i != lastin) <= + % + (n-2)*bool2int(x[j]==i) %% the Desrochers & Laporte '91 term + n - 1 + ) /\ + % Each node that is not in is numbered after the lastin node. + forall (i in S) ( + true + % (not ins[i]) <-> (n == order[i]) + ) + ) + endif; + +%-----------------------------------------------------------------------------% diff --git a/minizinc/lib-for-hypercubes/fzn_table_int.mzn b/minizinc/lib-for-hypercubes/fzn_table_int.mzn new file mode 100644 index 000000000..586b03278 --- /dev/null +++ b/minizinc/lib-for-hypercubes/fzn_table_int.mzn @@ -0,0 +1,16 @@ +%-----------------------------------------------------------------------------% +% A 'table' constraint table(x, T) represents the constraint x in T where we +% consider each row in T to be a tuple and T as a set of tuples. +% +% Linear version. +% +% See also the equality encoding of the 'element' constraint. +%-----------------------------------------------------------------------------% + +predicate fzn_table_int(array [int] of var int: x, array [int, int] of int: t) = + let { + set of int: it = index_set_1of2(t); + array [it] of var 0..1: lmda; + } in sum(lmda) = 1 /\ forall (j in index_set(x)) (sum (i in it) (t[i, j] * lmda[i]) = x[j]); + +%-----------------------------------------------------------------------------% diff --git a/minizinc/lib-for-hypercubes/options.mzn b/minizinc/lib-for-hypercubes/options.mzn new file mode 100644 index 000000000..034c2a757 --- /dev/null +++ b/minizinc/lib-for-hypercubes/options.mzn @@ -0,0 +1,207 @@ +/* +% Controls +% +*/ + +%-----------------------------------------------------------------------------% +%---------- USER and LAZY CUTS -----------------------------------------------% +/* + PLEASE NOTE: + If you export FZN file with lazy_constraint/user_cut annotations, + their declarations are not exported currently (as of 7.11.17). + WORKAROUND: when solving that fzn, add -G linear, + e.g., as follows: mzn-cplex -G linear model.fzn + * For Gurobi, the constraints marked as MIP_cut and/or MIP_lazy are added + * into the overall model and marked with the foll values of Lazy attribute: + * ::MIP_lazy 1 + * ::MIP_cut ::MIP_lazy 2 + * ::MIP_cut 3 + */ +ann: user_cut; +ann: lazy_constraint; +%%% comment away the below assignments (leaving, e.g., ann: MIP_cut;) to have them as normal constraints +%%% In particular, they may be used by redundant_constraint() and symmetry_breaking_constraint(), see redefs-2.0.2.mzn +ann: MIP_cut = user_cut; %% MIP_cut: make sure no feasible solutions are cut off +%% -- seems better on average but in CPLEX, wrong LB e.g. on carpet-cutting +ann: MIP_lazy = lazy_constraint; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% "GENERAL" constraints %%%%%%%%%%%%%%%%%%%%%%%%%%%%% +opt bool: fIndConstr; %% User option, e.g., with -D +%% Attention: as of MZN 2.4.3, you also need -DfMIPdomains=false +bool: fMZN__UseIndicators = if absent(fIndConstr) then false else deopt(fIndConstr) endif; %% Pass on indicator constraints +%% CPLEX 12.6.2 Concert: reifs give wrong result on 2012/amaze, so using implications only + +%% MAX/MIN +opt bool: MinMaxGeneral; %% User option, e.g., with -D +%% pass on min/max to the backend as fzn_array_float_minimum +bool: MZN__MinMaxGeneral = if absent(MinMaxGeneral) then false else deopt(MinMaxGeneral) endif; + +%% CUMULATIVE +opt bool: CumulativeSolverConfig; %% As set in share/minizinc/Preferences.json +opt bool: UseCumulative; %% User option, e.g., with -D +bool: MZN__Cumulative_Fixed_d_r = + if occurs(UseCumulative) then + deopt(UseCumulative) + elseif occurs(CumulativeSolverConfig) then + deopt(CumulativeSolverConfig) + else + false + endif; + +%% ORBISACK +opt bool: OrbisackSolverConfig; %% As set in share/minizinc/Preferences.json +opt bool: UseOrbisack; %% User option, e.g., with -D +bool: MZN__Orbisack = + if occurs(UseOrbisack) then + deopt(UseOrbisack) + elseif occurs(OrbisackSolverConfig) then + deopt(OrbisackSolverConfig) + else + false + endif; + +%% ORBITOPE +opt bool: OrbitopeSolverConfig; %% As set in share/minizinc/Preferences.json +opt bool: UseOrbitope; %% User option, e.g., with -D +bool: MZN__Orbitope = + if occurs(UseOrbitope) then + deopt(UseOrbitope) + elseif occurs(OrbitopeSolverConfig) then + deopt(OrbitopeSolverConfig) + else + false + endif; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Quadratic expressions %%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% --------------------------------------------------------------------------------------- % +%% Forward float_times as fzn_float_times +opt bool: QuadrFloatSolverConfig; %% As set in share/minizinc/Preferences.json +opt bool: QuadrFloat; %% User option, e.g., with -D +bool: MZN__QuadrFloat = + if occurs(QuadrFloat) then + deopt(QuadrFloat) + elseif occurs(QuadrFloatSolverConfig) then + deopt(QuadrFloatSolverConfig) + else + false + endif; + +%% Forward int_times as fzn_int_times +opt bool: QuadrIntSolverConfig; %% As set in share/minizinc/Preferences.json +opt bool: QuadrInt; %% User option, e.g., with -D +bool: QuadrIntFinal = + if occurs(QuadrInt) then + deopt(QuadrInt) + elseif occurs(QuadrIntSolverConfig) then + deopt(QuadrIntSolverConfig) + else + false + endif; +opt int: QuadrIntCard; %% Convert int_times to fzn_int_times if the minimum +%% of x, y's domain cardinalities as at least QuadrIntCard +int: MZN__QuadrIntCard = + if occurs(QuadrIntCard) then deopt(QuadrIntCard) elseif QuadrIntFinal then 0 else infinity endif; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Subtour elimination in circuit %%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% --------------------------------------------------------------------------------------- % +opt int: nSECcuts; %% 0,1: use MTZ formulation +int: nMZN__fSECcuts = if absent(nSECcuts) then 0 else deopt(nSECcuts) endif; %% 1,2: pass on circuit constraints to the MIP_solverinstance's cut gen + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% MIPdomains %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% --------------------------------------------------------------------------------------- % +%% Paper: % Belov, Stuckey, Tack, Wallace. Improved Linearization of Constraint Programming Models. CP 2016 Proceedings. +%%% The below option enables translation of domain constraints into the ...POST predicates. +%%% The code in MIPdomains.cpp processes them and also non-contiguous domains +%%% (only-range-domains is then standardly off). MIPdomains.cpp needs all the required +%%% __POST predicates to be declared to kick in. +opt bool: fMIPDomains; %% unified decomposition constraints (...__POST) to FlatZinc +opt bool: fMIPdomains; %% Can be defined from cmdline: -D "fMIPdomains=false" +bool: fPostprocessDomains = %% True to pass all domain-related + if absent(fMIPdomains) /\ absent(fMIPDomains) then + true + elseif not absent(fMIPdomains) then + deopt(fMIPdomains) + else + deopt(fMIPDomains) + endif; +opt bool: fMIPdomAux; +bool: fPostproDom_AUX = if absent(fMIPdomAux) then false else deopt(fMIPdomAux) endif; %% Specialized for aux_ constr +opt bool: fMIPdomDiff; +bool: fPostproDom_DIFF = %% Specialized for differences: x z=x-y<0 + if absent(fMIPdomDiff) then + false %% seems best for Gurobi, worse for CBC + else + deopt(fMIPdomDiff) + endif; + +mzn_opt_only_range_domains = not fPostprocessDomains; %% currently unused + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Avoid creating new int vars %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% --------------------------------------------------------------------------------------- % +opt bool: fAvoidNewInts; +bool: fAvoidNI = if absent(fAvoidNewInts) then false else deopt(fAvoidNewInts) endif; %% Actually this is only for ..._lin_..., not for just x-y +opt bool: fNewVarsInAuxEq; +bool: fAuxIntEqOLD00 = if absent(fNewVarsInAuxEq) then false else deopt(fNewVarsInAuxEq) endif; +bool: fAuxFloatEqOLD00 = if absent(fNewVarsInAuxEq) then false else deopt(fNewVarsInAuxEq) endif; + +%%%%%%%%%%%%%%%%%%%%% Redundant constraints ---------------------------------------------- % +bool: fMZN__IgnoreRedundantCumulative = false; +%% NOT WORKING NOW, use redefs_2.0.2.mzn: +%%%%% bool: fMZN__IgnoreAllUserRedundant=false; %% ignore all user-spec redundant constr + +%%%%%%%%%%%%%%%%%%%%% Element, minimuum convex hull --------------------------------------- % +opt bool: fXBZCuts01; %% orders 0, 1 +opt bool: fXBZCutGen; %% only works if Cuts01 +bool: fElementCutsXZ = false; %% Use simple XZ & XZB cuts for element +bool: fElementCutsXZB = if absent(fXBZCuts01) then false else deopt(fXBZCuts01) endif; +bool: fMinimumCutsXZ = false; %% Use simple XZ & XZB cuts for minimum +bool: fMinimumCutsXZB = if absent(fXBZCuts01) then false else deopt(fXBZCuts01) endif; +bool: fUseXBZCutGen = if absent(fXBZCutGen) then false else deopt(fXBZCutGen) endif; + +% ----------------------------------------------------------------------------------------- % +bool: fIntTimesBool = true; %% Special handling of multiplication with a boolean(*const) + +%-----------------------------------------------------------------------------% +% If not postprocessing domains: For unary encoding: maximal domain length to invoke it + +int: nMZN__UnarySizeMax_intTimes = 20; +int: nMZN__UnarySizeMax_cumul = 2000; +int: nMZN__UnarySizeMax_1step_regular = 20000; %% network-flow decomp in the regular constraint + +int: nMZN__UnaryLenMin__ALL = 1; %% can be used by the indiv. cases +int: nMZN__UnaryLenMax__ALL = 2000; %% can be used by the indiv. cases +% Some more detailed parameters +int: nMZN__UnaryLenMin_leq = 1; +int: nMZN__UnaryLenMin_neq = nMZN__UnaryLenMin__ALL; +int: nMZN__UnaryLenMin_eq = nMZN__UnaryLenMin__ALL; +int: nMZN__UnaryLenMax_leq = -1; +int: nMZN__UnaryLenMax_neq = nMZN__UnaryLenMax__ALL; +int: nMZN__UnaryLenMax_eq = nMZN__UnaryLenMax__ALL; +int: nMZN__UnaryLenMax_setIn = nMZN__UnaryLenMax__ALL; +int: nMZN__UnaryLenMax_setInReif = nMZN__UnaryLenMax__ALL; + +%-----------------------------------------------------------------------------% +% Strict inequality +% The relative epsilon +%%% Has the problem that when relating to upper bound of various differences, +%%% getting different absolute eps...? +%% float: float_lt_EPS_coef__ = 1e-03; ABANDONED 12.4.18 due to #207 +%%% Absolute one, used everywhere +%%% Might make no sense for floats with smaller domains etc. +opt float: float_EPS; +float: float_lt_EPS = if absent(float_EPS) then 1e-6 else deopt(float_EPS) endif; + +%-----------------------------------------------------------------------------% +%%% Set =true to PRINT TRACING messages for some constraints: +opt bool: fMIPTrace; +bool: mzn__my_trace_on = if absent(fMIPTrace) then false else deopt(fMIPTrace) endif; +test my_trace(string: msg) :: promise_total = if mzn__my_trace_on then trace(msg) else true endif; +test my_trace(string: msg, bool: bb) :: promise_total = + if mzn__my_trace_on then trace(msg, bb) else bb endif; +function var bool: my_trace(string: msg, var bool: bb) :: promise_total = + if mzn__my_trace_on then trace(msg, bb) else bb endif; +%%% Set =true to PRINT TRACING messages for the currently debugged constraints: +opt bool: fMIPTraceDBG; +bool: mzn__my_trace__DBG_on = if absent(fMIPTraceDBG) then false else deopt(fMIPTraceDBG) endif; +test my_trace__DBG(string: msg) :: promise_total = + if mzn__my_trace__DBG_on then trace(msg) else true endif; diff --git a/minizinc/lib-for-hypercubes/redefinitions-2.0.2.mzn b/minizinc/lib-for-hypercubes/redefinitions-2.0.2.mzn new file mode 100644 index 000000000..cd83dbb5f --- /dev/null +++ b/minizinc/lib-for-hypercubes/redefinitions-2.0.2.mzn @@ -0,0 +1,30 @@ +% This file contains redefinitions of standard builtins for version 2.0.2 +% that can be overridden by solvers. + +predicate symmetry_breaking_constraint(var bool: b) = (b); %:: MIP_lazy %:: MIP_cut %% MIP_cut wrong in CPLEX 12.6.3 +%% Symm breaking as lazy is 1% better in Gurobi 6.5.2 on the Challenges 2012-2015 +%% But caused a bug in 7.5.1 - switched off +%% true %% TO omit all symmetry_breaking_constraint's + +%% Make sure no feasible solutions are cut off: +predicate redundant_constraint(var bool: b) = (b); %:: MIP_cut +% true %% To omit all redundant_constraint's + +%% Linearized element: just call without shifting +predicate array_var_bool_element_nonshifted(var int: idx, array [int] of var bool: x, var bool: c) = + array_var_bool_element(idx, x, c); + +predicate array_var_int_element_nonshifted(var int: idx, array [int] of var int: x, var int: c) = + array_var_int_element(idx, x, c); + +predicate array_var_float_element_nonshifted( + var int: idx, + array [int] of var float: x, + var float: c, +) = array_var_float_element(idx, x, c); + +predicate array_var_set_element_nonshifted( + var int: idx, + array [int] of var set of int: x, + var set of int: c, +) = array_var_set_element(idx, x, c); diff --git a/minizinc/lib-for-hypercubes/redefinitions-2.0.mzn b/minizinc/lib-for-hypercubes/redefinitions-2.0.mzn new file mode 100644 index 000000000..2a4d0e2dd --- /dev/null +++ b/minizinc/lib-for-hypercubes/redefinitions-2.0.mzn @@ -0,0 +1,17 @@ +predicate bool_clause_reif(array [int] of var bool: p, array [int] of var bool: n, var bool: c) = + c = + ( + sum (i in index_set(p)) (bool2int(p[i])) - + sum (i in index_set(n)) (bool2int(n[i])) + + length(n) >= + 1 + ); + +predicate array_int_minimum(var int: m, array [int] of var int: x) = + array_int_minimum_I(m, [x[i] | i in index_set(x)]); +predicate array_int_maximum(var int: m, array [int] of var int: x) = + array_int_minimum_I(-m, [-x[i] | i in index_set(x)]); +predicate array_float_minimum(var float: m, array [int] of var float: x) = + array_float_minimum_I(m, [x[i] | i in index_set(x)]); +predicate array_float_maximum(var float: m, array [int] of var float: x) = + array_float_minimum_I(-m, [-x[i] | i in index_set(x)]); diff --git a/minizinc/lib-for-hypercubes/redefinitions-2.2.1.mzn b/minizinc/lib-for-hypercubes/redefinitions-2.2.1.mzn new file mode 100644 index 000000000..6335b5f81 --- /dev/null +++ b/minizinc/lib-for-hypercubes/redefinitions-2.2.1.mzn @@ -0,0 +1,5 @@ +% This file contains redefinitions of standard builtins for version 2.2.1 +% that can be overridden by solvers. + +/** @group flatzinc.int Constrains \a z = \(\a x ^ {\a y}\) */ +predicate int_pow_fixed(var int: x, int: y, var int: z) = int_pow(x, y, z); diff --git a/minizinc/lib-for-hypercubes/redefinitions.mzn b/minizinc/lib-for-hypercubes/redefinitions.mzn new file mode 100644 index 000000000..bf0d2567d --- /dev/null +++ b/minizinc/lib-for-hypercubes/redefinitions.mzn @@ -0,0 +1,748 @@ +/* +% FlatZinc built-in redefinitions for linear solvers. +% +% AUTHORS +% Sebastian Brand +% Gleb Belov (2015-) +% cf. Belov, Stuckey, Tack, Wallace. Improved Linearization of Constraint Programming Models. CP 2016. +*/ + +%----------------------------- BOOL2INT --------------------------------% +function var bool: reverse_map(var int: x) = (x == 1); +function bool: reverse_map(int: x) = (x == 1); + +predicate mzn_reverse_map_var(var bool: b) = let { var int: x = bool2int(b) } in true; + +function var int: bool2int( + var bool: x, +) + :: promise_total = + let { + var 0..1: b2i; + constraint (x = reverse_map(b2i)) :: is_reverse_map; + } in b2i; + +predicate bool_eq(var bool: x, var bool: y) = + %% trace(" bool_eq: \(x), \(y) \n") /\ + bool2int(x) == bool2int(y); + +predicate bool2int(var bool: x, var int: y) = y = bool2int(x); + +%---------------------------- BASIC (HALF)REIFS -----------------------------% + +include "options.mzn"; +include "redefs_bool_reifs.mzn"; +include "redefs_bool_imp.mzn"; + +include "domain_encodings.mzn"; +include "redefs_lin_reifs.mzn"; +include "redefs_lin_imp.mzn"; +include "redefs_lin_halfreifs.mzn"; + +include "nosets.mzn"; %% For set_le, set_lt ... Usind std/nosets +%% as long as the linearization is good. + +%-----------------------------------------------------------------------------% +% Strict inequality + +% Uncomment the following redefinition for FlatZinc MIP solver interfaces that +% do not support strict inequality. Note that it does not preserve equivalence +% (some solutions of the original problem may become invalid). + +predicate float_lt(var float: x :: promise_ctx_antitone, var float: y :: promise_ctx_monotone) = + % (x - y) <= (-float_lt_EPS_coef__)*max(abs(ub(x - y)), abs(ub(y-x))); + x <= y - float_lt_EPS; + +predicate float_lin_lt(array [int] of float: c, array [int] of var float: x, float: d) = + float_lt(sum (i in index_set(x)) (c[i] * x[i]), d); + +%-----------------------------------------------------------------------------% +% Minimum, maximum, absolute value +% Use unary as well? TODO + +predicate int_abs(var int: x, var int: z) = + %% The simplifications seem worse on league.mzn model90-18-20.dzn: + %% but the .lp seem to differ just by order...?? TODO + if lb(x) >= 0 then + z == x + elseif ub(x) <= 0 then + z == -x + else + let { + var bool: p; + } in z >= x /\ + z >= -x /\ + z >= 0 /\ % This is just for preprocessor + z <= max([ub(x), -lb(x)]) /\ % And this + % z <= x \/ z <= -x %% simple + aux_int_le_if_1(z, x, p) /\ %% even simpler + aux_int_le_if_0(z, -x, p) /\ + int_le_reif(0, x, p) % with reifs + %int_eq_reif(z, x, p) /\ + %int_eq_reif(z, -x, not p) + endif; + +predicate int_min(var int: x, var int: y, var int: z) = array_int_minimum(z, [x, y]); + +predicate int_max(var int: x, var int: y, var int: z) = array_int_maximum(z, [x, y]); + +predicate float_abs(var float: x, var float: z) = + if lb(x) >= 0.0 then + z == x + elseif ub(x) <= 0.0 then + z == -x + else + let { + var bool: p; + } in z >= x /\ + z >= -x /\ + z >= 0.0 /\ % This is just for preprocessor + z <= max([ub(x), -lb(x)]) /\ % And this + % z <= x \/ z <= -x + aux_float_le_if_1(z, x, (p)) /\ + aux_float_le_if_0(z, -x, (p)) + % /\ + % float_le_reif(0.0, x, p) % with reifs - no point for floats? TODO + % float_eq_reif(z, x, p) /\ + % float_eq_reif(z, -x, not p) + endif; + +predicate float_min(var float: x, var float: y, var float: z) = array_float_minimum(z, [x, y]); + +predicate float_max(var float: x, var float: y, var float: z) = array_float_maximum(z, [x, y]); + +predicate array_int_minimum_I( + var int: m, + array [int] of var int: x, +) = + let { + int: n = length(x); + constraint + assert(1 == min(index_set(x)), " array_int_minimum_I: argument indexed not from 1??"); + int: iMinUB = arg_min([ub(x[i]) | i in 1..n]); + int: MinUB = ub(x[iMinUB]); + set of int: sLBLess = {i | i in 1..n where lb(x[i]) < MinUB}; + set of int: sUBEqual = {i | i in 1..n where ub(x[i]) == MinUB}; + set of int: sActive = + if card(sLBLess intersect sUBEqual) > 0 then sLBLess else sLBLess union {iMinUB} endif; + } in if 1 == card(sActive) then + m == x[min(sActive)] + elseif MZN__MinMaxGeneral then + fzn_array_float_minimum(m, x) %% Send to backend + else + let { + array [1..n] of var int: p = [ + if i in sActive then let { var 0..1: pi } in pi else 0 endif | + i in 1..n, + ]; + constraint 1 == sum(p); + constraint m >= lb_array(x); + constraint m <= MinUB; + } in forall ( + i in index_set(x), + ) ( + if i in sActive then %% for at least 1 element + m <= x[i] /\ aux_int_ge_if_1(m, x[i], p[i]) + endif + ) %% -- exclude too big x[i] + endif; + +predicate array_float_minimum_I(var float: m, array [int] of var float: x) = + let { + int: n = length(x); + constraint + assert(1 == min(index_set(x)), " array_float_minimum_I: argument indexed not from 1??"); + int: iMinUB = arg_min([ub(x[i]) | i in 1..n]); + float: MinUB = ub(x[iMinUB]); + set of int: sLBLess = {i | i in 1..n where lb(x[i]) < MinUB}; + set of int: sUBEqual = {i | i in 1..n where ub(x[i]) == MinUB}; + set of int: sActive = + if card(sLBLess intersect sUBEqual) > 0 then sLBLess else sLBLess union {iMinUB} endif; + } in if 1 == card(sActive) then + m == x[min(sActive)] + elseif MZN__MinMaxGeneral then + fzn_array_float_minimum(m, x) %% Send to backend + else + let { + array [1..n] of var int: p = [ + if i in sActive then let { var 0..1: pi } in pi else 0 endif | + i in 1..n, + ]; + constraint 1 == sum(p); + constraint m >= lb_array(x); + constraint m <= MinUB; + } in forall ( + i in index_set(x), + ) ( + if i in sActive then %% for at least 1 element + m <= x[i] /\ aux_float_ge_if_1(m, x[i], p[i]) + endif + ) /\ %% -- exclude too big x[i] + if card(sActive) > 1 /\ fMinimumCutsXZ then + let { + array [int] of float: AL = [lb(x[i]) | i in 1..n]; + array [int] of int: srt = sort_by([i | i in 1..n], AL); + %indices of lb in sorted order + array [int] of float: AL_srt = [AL[srt[i]] | i in 1..n]; + array [int] of float: AU_srt = [ub(x[srt[i]]) | i in 1..n]; + array [int] of float: AM_srt = AL_srt ++ [MinUB]; + } in %% -- these are z-levels of extreme points + forall ( + i in + 2..n + 1 + where AM_srt[i] <= MinUB /\ %% this is a new "start level" + AM_srt[i] != AM_srt[i - 1], %% and would produce a new cut + ) ( + m >= + AM_srt[i] - + sum (j in 1..i - 1 where AL_srt[j] < AM_srt[i] /\ AL_srt[j] < AU_srt[j]) ( + (AU_srt[j] - x[srt[j]]) * (AM_srt[i] - AL_srt[j]) / (AU_srt[j] - AL_srt[j]) + ) :: MIP_cut + ) + else + true + endif /\ + if card(sActive) > 1 /\ fMinimumCutsXZB then + array_var_float_element__XBZ_lb([-x[i] | i in sActive], [p[i] | i in sActive], -m) + :: MIP_cut + else + true + endif + endif; + +%-----------------------------------------------------------------------------% +% Multiplication and division + +predicate int_div(var int: x, var int: y, var int: q) = q == aux_int_division_modulo_fn(x, y)[1]; + +predicate int_mod(var int: x, var int: y, var int: r) = r == aux_int_division_modulo_fn(x, y)[2]; + +function array [int] of var int: aux_int_division_modulo_fn(var int: x, var int: y) = + let { + %% Domain of q + set of int: dom_q = + if lb(y) * ub(y) > 0 then + let { + set of int: EP = {ub(x) div ub(y), ub(x) div lb(y), lb(x) div ub(y), lb(x) div lb(y)}; + } in min(EP)..max(EP) + else + let { int: mm = max(abs(lb(x)), abs(ub(x))) } in -mm..mm %% TODO case when -1 or 1 not in dom(x) + endif; + var dom_q: q; + int: by = max(abs(lb(y)), abs(ub(y))); + var -by + 1..by - 1: r; + constraint x = y * q + r; + constraint 0 <= x -> 0 <= r; %% which is 0 > x \/ 0 <= r + constraint x < 0 -> r <= 0; %% which is x >= 0 \/ r <= 0 + % abs(r) < abs(y) + var 1..max(abs(lb(y)), abs(ub(y))): w = abs(y); + constraint w > r /\ w > -r; + } in [q, r]; + +%% Can also have int_times(var float, var int) ......... TODO + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +predicate float_div(var float: x, var float: y, var float: q) = x == y * q; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +predicate int_times( + var int: x, + var int: y, + var int: z, +) = + if is_fixed(x) then + z == fix(x) * y %%%%% Need to use fix() otherwise added to CSE & nothing happens + elseif is_fixed(y) then + z == x * fix(y) + elseif is_same(x, y) then + z == pow(x, 2) + elseif 0..1 == dom(x) /\ 0..1 == dom(y) then + bool_and__INT(x, y, z) + elseif card(dom(x)) == 2 /\ card(dom(y)) == 2 /\ 0 in dom(x) /\ 0 in dom(y) then + let { + var 0..1: xn; + var 0..1: yn; + var 0..1: zn; + constraint x = xn * max(dom(x) diff {0}); + constraint y = yn * max(dom(y) diff {0}); + constraint z = zn * max(dom(x) diff {0}) * max(dom(y) diff {0}); + } in bool_and__INT(xn, yn, zn) + elseif min(card(dom(x)), card(dom(y))) >= MZN__QuadrIntCard then + fzn_int_times(x, y, z) + elseif card(dom(x)) * card(dom(y)) > nMZN__UnarySizeMax_intTimes \/ + ( + fIntTimesBool /\ + ( + %% Peter's idea for *bool. More optimal but worse values on carpet cutting. + (card(dom(x)) == 2 /\ 0 in dom(x)) \/ (card(dom(y)) == 2 /\ 0 in dom(y)) + ) + ) then + %% PARAM + %% ALSO NO POINT IF <=4. TODO + if card(dom(x)) > card(dom(y)) \/ + (card(dom(x)) == card(dom(y)) /\ 0 in dom(y) /\ not (0 in dom(x))) then + int_times(y, x, z) + else + let { + set of int: s = lb(x)..ub(x); + set of int: r = {lb(x) * lb(y), lb(x) * ub(y), ub(x) * lb(y), ub(x) * ub(y)}; + array [s] of var min(r)..max(r): ady = + array1d(s, [if d in dom(x) then d * y else min(r) endif | d in s]); + } in ady[x] = z %% use element() + endif + else + int_times_unary(x, {}, y, z) + endif; + +%% domx__ can be used to narrow domain... NOT IMPL. +predicate int_times_unary(var int: x, set of int: domx__, var int: y, var int: z) = + let { + set of int: r = {lb(x) * lb(y), lb(x) * ub(y), ub(x) * lb(y), ub(x) * ub(y)}; + %% set of int: domx = if card(domx__)>0 then domx__ else dom(x) endif, + array [int, int] of var int: pp = eq_encode(x, y); + } in z >= min(r) /\ + z <= max(r) /\ + z == sum (i in index_set_1of2(pp), j in index_set_2of2(pp)) (i * j * pp[i, j]) /\ + forall (i in index_set_1of2(pp), j in index_set_2of2(pp) where not ((i * j) in dom(z))) ( + pp[i, j] == 0 + ); + +predicate int_times_unary__NOFN(var int: x, set of int: domx__, var int: y, var int: z) = + let { + set of int: r = {lb(x) * lb(y), lb(x) * ub(y), ub(x) * lb(y), ub(x) * ub(y)}; + %% set of int: domx = if card(domx__)>0 then domx__ else dom(x) endif, + array [int] of var int: pX = eq_encode(x); + array [int] of var int: pY = eq_encode(y); + array [int] of int: valX = [v | v in index_set(pX)]; %% NOT domx. + array [int] of int: valY = [v | v in index_set(pY)]; %% -- according to eq_encode! + array [index_set(valX), index_set(valY)] of var 0..1: pp; %% both dim 1.. + } in if is_fixed(x) \/ is_fixed(y) then + z == x * y + else + z >= min(r) /\ + z <= max(r) /\ + sum(pp) == 1 /\ + z == sum (i in index_set(valX), j in index_set(valY)) (valX[i] * valY[j] * pp[i, j]) /\ + forall (i in index_set(valX)) (pX[valX[i]] == sum (j in index_set(valY)) (pp[i, j])) /\ + forall (j in index_set(valY)) (pY[valY[j]] == sum (i in index_set(valX)) (pp[i, j])) + endif; + +predicate float_times(var float: x, var float: y, var float: z) = + if is_fixed(x) then + z == fix(x) * y %%%%% Need to use fix() otherwise added to CSE & nothing happens + elseif is_fixed(y) then + z == x * fix(y) + elseif MZN__QuadrFloat then + fzn_float_times(x, y, z) + else + abort( + "Unable to create linear formulation for the `float_times(\(x), \(y), \(z))`\n" ++ + "\tconstraint. To flatten this instance a quadratic constraint is required, but the\n" ++ + "\tusage of these constraints is currently disabled for the selected solver. Define\n" ++ + "\t`QuadrFloat=true` if your solver supports quadratic constraints, or use\n" ++ + "\tinteger variables.", + ) + endif; + +%%%Define int_pow +predicate int_pow(var int: x, var int: y, var int: r) = + let { + array [int, int] of int: x2y = + array2d(lb(x)..ub(x), lb(y)..ub(y), [pow(X, Y) | X in lb(x)..ub(x), Y in lb(y)..ub(y)]); + } in r == x2y[x, y]; + +%%% Adding a version returning float for efficiency +/** @group builtins.arithmetic Return \(\a x ^ {\a y}\) */ +function var float: pow_float(var int: x, var int: y) = + let { + int: yy = if is_fixed(y) then fix(y) else -1 endif; + } in if yy = 0 then + 1 + elseif yy = 1 then + x + else + let { + var float: r :: is_defined_var; + constraint int_pow_float(x, y, r) :: defines_var(r); + } in r + endif; +%%%Define int_pow_float +predicate int_pow_float(var int: x, var int: y, var float: r) = + let { + array [int, int] of float: x2y = + array2d(lb(x)..ub(x), lb(y)..ub(y), [pow(X, Y) | X in lb(x)..ub(x), Y in lb(y)..ub(y)]); + } in r == x2y[x, y]; + +%-----------------------------------------------------------------------------% +% Array 'element' constraints + +predicate array_bool_element(var int: x, array [int] of bool: a, var bool: z) = + array_int_element(x, arrayXd(a, [bool2int(a[i]) | i in index_set(a)]), bool2int(z)); + +predicate array_var_bool_element(var int: x, array [int] of var bool: a, var bool: z) = + array_var_int_element(x, arrayXd(a, [bool2int(a[i]) | i in index_set(a)]), bool2int(z)); + +predicate array_int_element(var int: i00, array [int] of int: a, var int: z) = + let { + set of int: ix = index_set(a); + constraint i00 in {i | i in ix where a[i] in dom(z)}; + } in %%% Tighten domain of i00 before dMin/dMax + let { + int: dMin = min (i in dom(i00)) (a[i]); + int: dMax = max (i in dom(i00)) (a[i]); + } in if dMin == dMax then + z == dMin + else + z >= + dMin /\ + z <= + dMax /\ + ( + let { + int: nUBi00 = max(dom(i00)); + int: nLBi00 = min(dom(i00)); + int: nMinDist = min (i in nLBi00..nUBi00 - 1) (a[i + 1] - a[i]); + int: nMaxDist = max (i in nLBi00..nUBi00 - 1) (a[i + 1] - a[i]); + } in if nMinDist == nMaxDist then %% The linear case + z == a[nLBi00] + nMinDist * (i00 - nLBi00) + else + let { + array [int] of var int: p = eq_encode(i00); %% this needs i00 in ix + } in %% Faster flattening than (i==i00) @2a9df1f7 + sum (i in dom(i00)) (a[i] * p[i]) == z + endif + ) + endif; + +predicate array_var_int_element(var int: i00, array [int] of var int: a, var int: z) = + let { + constraint i00 in {i | i in index_set(a) where 0 < card(dom(a[i]) intersect dom(z))}; + } in %% finish domain first + let { + int: minLB = min (i in dom(i00)) (lb(a[i])); + int: maxUB = max (i in dom(i00)) (ub(a[i])); + } in if minLB == + maxUB then + z == + minLB + else + z >= minLB /\ + z <= maxUB /\ + if {0, 1} == dom(i00) then /*ub(i00)-lb(i00)==1*/ /*2==card( dom( i00 ) )*/ + aux_int_eq_if_1(z, a[lb(i00)], (ub(i00) - i00)) /\ + aux_int_eq_if_1(z, a[ub(i00)], (i00 - lb(i00))) + else + let { + array [int] of var int: p = eq_encode(i00); %% this needs i00 in ix + } in %% Faster flattening than (i==i00) @2a9df1f7 + forall (i in dom(i00)) (aux_int_eq_if_1(z, a[i], p[i])) + endif + endif; + +predicate array_float_element(var int: i00, array [int] of float: a, var float: z) = + let { + set of int: ix = index_set(a); + constraint i00 in {i | i in ix where a[i] >= lb(z) /\ a[i] <= ub(z)}; + } in %%% Tighten domain of i00 before dMin/dMax + let { + float: dMin = min (i in dom(i00)) (a[i]); + float: dMax = max (i in dom(i00)) (a[i]); + } in if dMin == dMax then + z == dMin + else + z >= + dMin /\ + z <= + dMax /\ + ( + let { + int: nUBi00 = max(dom(i00)); + int: nLBi00 = min(dom(i00)); + float: nMinDist = min (i in nLBi00..nUBi00 - 1) (a[i + 1] - a[i]); + float: nMaxDist = max (i in nLBi00..nUBi00 - 1) (a[i + 1] - a[i]); + } in if nMinDist == nMaxDist then %% The linear case + z == a[nLBi00] + nMinDist * (i00 - nLBi00) + else + let { + array [int] of var int: p = eq_encode(i00); %% this needs i00 in ix + } in %% Faster flattening than (i==i00) @2a9df1f7 + sum (i in dom(i00)) (a[i] * p[i]) == z + endif + ) + endif; + +predicate array_var_float_element(var int: i00, array [int] of var float: a, var float: z) = + let { + set of int: ix = index_set(a); + constraint i00 in {i | i in ix where lb(a[i]) <= ub(z) /\ ub(a[i]) >= lb(z)}; + } in %% finish domain first + let { + float: minLB = min (i in dom(i00)) (lb(a[i])); + float: maxUB = max (i in dom(i00)) (ub(a[i])); + } in if minLB == + maxUB then + z == + minLB + else + z >= minLB /\ + z <= maxUB /\ + if {0, 1} == dom(i00) then /*ub(i00)-lb(i00)==1*/ /*2==card( dom( i00 ) )*/ + aux_float_eq_if_1(z, a[lb(i00)], (ub(i00) - i00)) /\ + aux_float_eq_if_1(z, a[ub(i00)], (i00 - lb(i00))) + else + %%% The convexified bounds seem slow for ^2 and ^3 equations: + % sum(i in dom(i01))( lb(a[i]) * (i==i00) ) <= z /\ %% convexify lower bounds + % sum(i in dom(i01))( ub(a[i]) * (i==i00) ) >= z /\ %% convexify upper bounds + let { + array [int] of var int: p = eq_encode(i00); %% this needs i00 in ix + } in %% Faster flattening than (i==i00) @2a9df1f7 + forall (i in dom(i00)) (aux_float_eq_if_1(z, a[i], p[i])) /\ + %% Cuts: + if fElementCutsXZ then + array_var_float_element__ROOF([a[i] | i in dom(i00)], z) :: MIP_cut /\ %% these 2 as user cuts - too slow + array_var_float_element__ROOF([-a[i] | i in dom(i00)], -z) :: MIP_cut %% or even skip them + else + true + endif /\ + if fElementCutsXZB then + array_var_float_element__XBZ_lb([a[i] | i in dom(i00)], [p[i] | i in dom(i00)], z) + :: MIP_cut /\ + array_var_float_element__XBZ_lb([-a[i] | i in dom(i00)], [p[i] | i in dom(i00)], -z) + :: MIP_cut + else + true + endif + endif + endif; + +%%% Facets on the upper surface of the z-a polytope +%%% Possible parameter: maximal number of first cuts taken only +predicate array_var_float_element__ROOF(array [int] of var float: a, var float: z) = + let { + set of int: ix = index_set(a); + int: n = length(a); + array [int] of float: AU = [ub(a[i]) | i in 1..n]; + array [int] of int: srt_ub = sort_by([i | i in 1..n], AU); + %indices of ub sorted up + array [int] of float: AU_srt_ub = [ub(a[srt_ub[i]]) | i in 1..n]; + array [int] of float: AL_srt_ub = [lb(a[srt_ub[i]]) | i in 1..n]; + array [int] of float: MaxLBFrom = [ + max (j in index_set(AL_srt_ub) where j >= i) (AL_srt_ub[j]) | + i in 1..n, + ]; %% direct, O(n^2) + array [int] of float: ULB = [ + if 1 == i then MaxLBFrom[1] else max([AU_srt_ub[i - 1], MaxLBFrom[i]]) endif | + i in 1..n, + ]; + } in %%% "ROOF" + forall ( + i in 1..n where if i == n then true else ULB[i] != ULB[i + 1] endif, %% not the same base bound + ) ( + z <= + ULB[i] + + sum ( + j in i..n where AU_srt_ub[i] != AL_srt_ub[i], %% not a const + ) ((AU_srt_ub[j] - ULB[i]) * (a[srt_ub[j]] - AL_srt_ub[j]) / (AU_srt_ub[j] - AL_srt_ub[j])) + ); + +predicate array_var_float_element__XBZ_lb( + array [int] of var float: x, + array [int] of var int: b, + var float: z, +) = + if fUseXBZCutGen then + array_var_float_element__XBZ_lb__cutgen(x, b, z) :: MIP_cut + else + %% Adding some cuts a priori, also to make solver extract the variables + let { + int: i1 = + min(index_set(x)); + } in ( + z <= sum (i in index_set(x)) (ub(x[i]) * b[i]) %:: MIP_cut -- does not work to put them here TODO + ) /\ + forall ( + i in index_set(x) intersect i1..(i1 + 19), %% otherwise too many on amaze2 + ) ( + assert( + lb(x[i]) == -ub(-x[i]) /\ ub(x[i]) == -lb(-x[i]), + " negated var's bounds should swap ", + ) /\ + z <= x[i] + sum (j in index_set(x) where i != j) ((ub(x[j]) - lb(x[i])) * b[j]) + ) /\ %:: MIP_cut %% (ub_j-lb_i) * b_j + forall (i in index_set(x) intersect i1..(i1 + 19)) ( + z <= ub(x[i]) * b[i] + sum (j in index_set(x) where i != j) (x[j] + lb(x[j]) * (b[j] - 1)) + ) /\ %:: MIP_cut + (z <= sum (i in index_set(x)) (x[i] + lb(x[i]) * (b[i] - 1))) %:: MIP_cut + endif; + +%-----------------------------------------------------------------------------% +% Set constraints +%% ----------------------------------------------- (NO) SETS ---------------------------------------------- +% XXX only for a fixed set here, general see below. +% Normally not called because all plugged into the domain. +% Might be called instead of set_in(x, var set of int s) if s gets fixed? +predicate set_in(var int: x, set of int: s__) = + let { + set of int: s = if has_bounds(x) then s__ intersect dom(x) else s__ endif; + constraint min(s) <= x; + constraint x <= max(s); + } in if s = min(s)..max(s) then + true + elseif fPostprocessDomains then + set_in__POST(x, s) + else + %% Update eq_encode + let { + array [int] of var int: p = eq_encode(x); + } in forall (i in index_set(p) diff s) (p[i] == 0) + % let { + % array[int] of int: sL = [ e | e in s where not (e - 1 in s) ]; + % array[int] of int: sR = [ e | e in s where not (e + 1 in s) ]; + % array [index_set(sR)] of var 0..1: B; + % constraint assert(length(sR)==length(sL), "N of lb and ub of sub-intervals of a set should be equal"); + % } in + % sum(B) = 1 %% use indicators + % /\ + % x >= sum(i in index_set(sL))(B[i]*sL[i]) + % /\ + % x <= sum(i in index_set(sR))(B[i]*sR[i]) + endif; + +%%% for a fixed set +predicate set_in_reif(var int: x, set of int: s__, var bool: b) = + if is_fixed(b) then + if fix(b) then x in s__ else x in dom(x) diff s__ endif + elseif has_bounds(x) /\ not (s__ subset dom(x)) then + b <-> x in s__ intersect dom(x) %% Use CSE + else + let { + set of int: s = if has_bounds(x) then s__ intersect dom(x) else s__ endif; + } in ( + if dom(x) subset s then + b == true + elseif card(dom(x) intersect s) == 0 then + b == false + elseif fPostprocessDomains then + set_in_reif__POST(x, s, b) + else + %% Bad. Very much so for CBC. 27.06.2019: elseif s == min(s)..max(s) then + %% b <-> (min(s) <= x /\ x <= max(s)) + if card(dom(x)) <= nMZN__UnaryLenMax_setInReif then %% PARAM TODO + let { + array [int] of var int: p = eq_encode(x); + } in sum (i in s intersect dom(x)) (p[i]) == bool2int(b) + else + bool2int(b) == fVarInBigSetOfInt(x, s) + endif + endif + ) + endif; + +% Alternative +predicate alt_set_in_reif(var int: x, set of int: s, var bool: b) = + b <-> + exists (i in 1..length([0 | e in s where not (e - 1 in s)])) ( + let { + int: l = [e | e in s where not (e - 1 in s)][i]; + int: r = [e | e in s where not (e + 1 in s)][i]; + } in l <= x /\ x <= r + ); + +%%% for a fixed set +predicate set_in_imp(var int: x, set of int: s__, var bool: b) = + if is_fixed(b) then + if fix(b) then x in s__ else true endif + elseif has_bounds(x) /\ not (s__ subset dom(x)) then + b -> x in s__ intersect dom(x) %% Use CSE + else + let { + set of int: s = if has_bounds(x) then s__ intersect dom(x) else s__ endif; + } in ( + if dom(x) subset s then + true + elseif card(dom(x) intersect s) == 0 then + b == false + elseif s == min(s)..max(s) then + (b -> min(s) <= x) /\ (b -> x <= max(s)) + else + if card(dom(x)) <= nMZN__UnaryLenMax_setInReif then %% PARAM TODO + let { + array [int] of var int: p = eq_encode(x); + } in sum (i in s intersect dom(x)) (p[i]) >= bool2int(b) + else + bool2int(b) <= fVarInBigSetOfInt(x, s) + endif + endif + ) + endif; + +function var 0..1: fVarInBigSetOfInt(var int: x, set of int: s) = + let { + array [int] of int: sL = [e | e in s where not (e - 1 in s)]; + array [int] of int: sR = [e | e in s where not (e + 1 in s)]; + constraint + assert(length(sR) == length(sL), "N of lb and ub of sub-intervals of a set should be equal"); + } in sum (i in index_set(sL)) (bool2int(x >= sL[i] /\ x <= sR[i])); %% use indicators + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% OTHER SET STUFF COMING FROM nosets.mzn %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%-----------------------------------------------------------------------------% +%-----------------------------------------------------------------------------% + +function ann: bool_search(array [int] of var bool: x, ann: a1, ann: a2, ann: a3) = + int_search([bool2int(x_i) | x_i in x], a1, a2, a3); + +function ann: warm_start(array [int] of var bool: x, array [int] of bool: v) = + warm_start([bool2int(x[i]) | i in index_set(x)], [bool2int(v[i]) | i in index_set(v)]); + +function ann: sat_goal(var bool: b) = int_max_goal(b); +annotation int_max_goal(var int: x); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% DOMAIN POSTPROCESSING BUILT-INS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Single variable: x = d <-> x_eq_d[d] +predicate equality_encoding__POST(var int: x, array [int] of var int: x_eq_d); + +%%%%%%% var int: b: bool2int is a reverse_map, not passed to .fzn +predicate set_in__POST(var int: x, set of int: s__); +predicate set_in_reif__POST(var int: x, set of int: s__, var int: b); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% LOGICAL CONSTRAINTS TO THE SOLVER %%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%% var int: b: bool2int is a reverse_map, not passed to .fzn => REPEAT TESTS. TODO +predicate int_lin_eq_reif__IND( + array [int] of int: c, + array [int] of var int: x, + int: d, + var int: b, +); +predicate int_lin_le_reif__IND( + array [int] of int: c, + array [int] of var int: x, + int: d, + var int: b, +); +predicate int_lin_ne__IND(array [int] of int: c, array [int] of var int: x, int: d); +predicate aux_int_le_zero_if_0__IND(var int: x, var int: b); +predicate float_lin_le_reif__IND( + array [int] of float: c, + array [int] of var float: x, + float: d, + var int: b, +); +predicate aux_float_eq_if_1__IND(var float: x, var float: y, var int: b); +predicate aux_float_le_zero_if_0__IND(var float: x, var int: b); + +predicate array_int_minimum__IND(var int: m, array [int] of var int: x); +predicate array_int_maximum__IND(var int: m, array [int] of var int: x); +predicate array_float_minimum__IND(var float: m, array [int] of var float: x); +predicate array_float_maximum__IND(var float: m, array [int] of var float: x); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% XBZ cut generator, currently CPLEX only %%%%%%%%%%%%%%%%%%%%%%%%%% +predicate array_var_float_element__XBZ_lb__cutgen( + array [int] of var float: x, + array [int] of var int: b, + var float: z, +); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Quadratic expressions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%5 +predicate fzn_float_times(var float: a, var float: b, var float: c); +predicate fzn_int_times(var int: a, var int: b, var int: c); +predicate fzn_array_float_minimum(var float: m, array [int] of var float: x); diff --git a/minizinc/lib-for-hypercubes/redefs_bool_imp.mzn b/minizinc/lib-for-hypercubes/redefs_bool_imp.mzn new file mode 100644 index 000000000..52593804d --- /dev/null +++ b/minizinc/lib-for-hypercubes/redefs_bool_imp.mzn @@ -0,0 +1,94 @@ +include "redefs_lin_halfreifs.mzn"; + +% SIMPLE BOOLEAN LOGIC +% TODO: why not check "is_fixed(r)" everywhere?? + +predicate bool_eq_imp(var bool: p, var bool: q, var bool: r :: promise_ctx_antitone) = + if is_fixed(r) then + if fix(r) then p = q else true endif + else + let { + var int: x = bool2int(p); + var int: y = bool2int(q); + var int: z = bool2int(r); + } in x + z <= y + 1 /\ y + z <= x + 1 + endif; + +predicate bool_ne_imp(var bool: p, var bool: q, var bool: r :: promise_ctx_antitone) = + bool_xor_imp(p, q, r); + +predicate bool_le_imp( + var bool: p :: promise_ctx_antitone, + var bool: q :: promise_ctx_monotone, + var bool: r :: promise_ctx_antitone, +) = + let { + var int: x = bool2int(p); + var int: y = bool2int(q); + var int: z = bool2int(r); + } in 1 - x + y >= z; + +predicate bool_lt_imp( + var bool: p :: promise_ctx_antitone, + var bool: q :: promise_ctx_monotone, + var bool: r :: promise_ctx_antitone, +) = bool_and_imp(not p, q, r); + +predicate bool_or_imp( + var bool: p :: promise_ctx_monotone, + var bool: q :: promise_ctx_monotone, + var bool: r :: promise_ctx_antitone, +) = array_bool_or_imp([p, q], r); + +predicate bool_and_imp( + var bool: p :: promise_ctx_monotone, + var bool: q :: promise_ctx_monotone, + var bool: r :: promise_ctx_antitone, +) = array_bool_and_imp([p, q], r); + +predicate bool_xor_imp(var bool: p, var bool: q, var bool: r :: promise_ctx_antitone) = + let { + var int: x = bool2int(p); + var int: y = bool2int(q); + var int: z = bool2int(r); + } in x + y >= z /\ x + y + z <= 2; + +% BOOLEAN ARRAY OPERATIONS +predicate array_bool_or_imp( + array [int] of var bool: a :: promise_ctx_monotone, + var bool: b :: promise_ctx_antitone, +) = + if forall (i in index_set(a)) (is_fixed(a[i]) /\ not fix(a[i])) then + not b + elseif exists (i in index_set(a)) (is_fixed(a[i]) /\ fix(a[i])) then + true + else + let { + array [index_set(a)] of var bool: a1; + var int: x = bool2int(b); + } in forall (i in index_set(a)) (a1[i] -> a[i]) /\ sum(a1) = x + endif; + +predicate array_bool_and_imp( + array [int] of var bool: a :: promise_ctx_monotone, + var bool: b :: promise_ctx_antitone, +) = + if is_fixed(b) then + if fix(b) then forall (i in index_set(a)) (a[i]) else true endif + elseif forall (i in index_set(a)) (is_fixed(a[i]) /\ fix(a[i])) then + true + else + let { + var int: x = bool2int(b); + array [index_set(a)] of var int: c = + array1d(index_set(a), [bool2int(a[i]) | i in index_set(a)]); + } in forall (i in index_set(c)) (c[i] >= x) + endif; + +%% No var int d, sorry TODO: why not??? +predicate bool_lin_eq_imp( + array [int] of int: c, + array [int] of var bool: x, + int: d, + var bool: b :: promise_ctx_antitone, +) = aux_int_eq_if_1(sum (i in index_set(x)) (c[i] * bool2int(x[i])), d, bool2int(b)); diff --git a/minizinc/lib-for-hypercubes/redefs_bool_reifs.mzn b/minizinc/lib-for-hypercubes/redefs_bool_reifs.mzn new file mode 100644 index 000000000..c313b263d --- /dev/null +++ b/minizinc/lib-for-hypercubes/redefs_bool_reifs.mzn @@ -0,0 +1,178 @@ +/* +% FlatZinc built-in redefinitions for linear solvers. +% +% AUTHORS +% Sebastian Brand +% Gleb Belov +*/ + +%-----------------------------------------------------------------------------% +% +% Logic operations +% Use indicators for reifs (CPLEX)? Seems weak. +% +%-----------------------------------------------------------------------------% + +predicate bool_not(var bool: p) = bool2int(p) = 0; + +predicate bool_not(var bool: p, var bool: q) = bool2int(p) + bool2int(q) = 1; + +predicate bool_and(var bool: p, var bool: q, var bool: r) = + % my_trace(" bool_and: \(p) /\\ \(q) <-> \(r) \n") /\ + if false then + int_lin_le_reif__IND([-1, -1], [p, q], -2, r) + else + array_bool_and([p, q], r) + % bool_and__INT(bool2int(p), bool2int(q), bool2int(r)) + endif; + +predicate bool_and__INT(var int: x, var int: y, var int: z) = + x + y <= z + 1 /\ + %% x + y >= z * 2; % weak + x >= z /\ + y >= z; % strong + +predicate bool_or(var bool: p, var bool: q, var bool: r) = + if false then + int_lin_le_reif__IND([-1, -1], [p, q], -1, r) + elseif true then + array_bool_or([p, q], r) + else + let { + var int: x = bool2int(p); + var int: y = bool2int(q); + var int: z = bool2int(r); + } in x + y >= z /\ + % x + y <= z * 2; % weak + x <= z /\ + y <= z % strong + endif; + +predicate bool_xor(var bool: p, var bool: q) = 1 == p + q; + +predicate bool_xor(var bool: p, var bool: q, var bool: r) = + if false then + % int_lin_eq_reif__IND( [1, 1], [p, q], 1, r) /\ + true + else + let { + var int: x = bool2int(p); + var int: y = bool2int(q); + var int: z = bool2int(r); + } in x <= y + z /\ y <= x + z /\ z <= x + y /\ x + y + z <= 2 + endif; + +predicate bool_eq_reif(var bool: p, var bool: q, var bool: r) = + %% trace(" bool_eq_reif: \(p), \(q), \(r) \n") /\ + if is_fixed(r) then % frequent case + if fix(r) = true then p = q else bool_not(p, q) endif + elseif is_fixed(q) then + if fix(q) = true then p = r else bool_not(p, r) endif + elseif is_fixed(p) then + if fix(p) = true then q = r else bool_not(q, r) endif + elseif false then + % int_lin_eq_reif__IND( [1, -1], [p, q], 0, r) /\ + true + else + let { + var int: x = bool2int(p); + var int: y = bool2int(q); + var int: z = bool2int(r); + } in x + y <= z + 1 /\ x + z <= y + 1 /\ y + z <= x + 1 /\ x + y + z >= 1 + endif; + +predicate bool_ne_reif(var bool: p, var bool: q, var bool: r) = bool_xor(p, q, r); + +predicate bool_le(var bool: p :: promise_ctx_antitone, var bool: q :: promise_ctx_monotone) = + let { + var int: x :: promise_ctx_antitone = bool2int(p); + var int: y :: promise_ctx_monotone = bool2int(q); + } in x <= y; + +predicate bool_le_reif(var bool: p, var bool: q, var bool: r) = + if false then + % int_lin_le_reif__IND( [1, -1], [p, q], 0, r) /\ + true + else + let { + var int: x = bool2int(p); + var int: y = bool2int(q); + var int: z = bool2int(r); + } in 1 - x + y >= z /\ + %% /\ 1 - x + y <= z * 2 not needed + 1 - x <= z /\ + y <= z % strong + endif; + +predicate bool_lt(var bool: p :: promise_ctx_antitone, var bool: q :: promise_ctx_monotone) = + not p /\ q; + +predicate bool_lt_reif(var bool: p, var bool: q, var bool: r) = (not p /\ q) <-> r; + +%-----------------------------------------------------------------------------% + +%% Reified disjunction +predicate array_bool_or(array [int] of var bool: a, var bool: b) = + if exists (i in index_set(a)) (is_fixed(a[i]) /\ fix(a[i])) then + b + elseif is_fixed(b) then % frequent case + if fix(b) = true then + sum (i in index_set(a)) (bool2int(a[i])) >= 1 %% >=1 seems better for MIPDomains... 5.4.19 + else + forall (i in index_set(a)) (not a[i]) + endif + else + let { + var int: x = bool2int(b); + array [1..length(a)] of var int: c = [bool2int(a[i]) | i in index_set(a)]; + } in sum(c) >= x /\ + % sum(c) <= x * length(a) % weak + forall (i in index_set(a)) (x >= c[i]) % strong + endif; + +%% Reified conjunction +predicate array_bool_and(array [int] of var bool: a, var bool: b) = + if exists (i in index_set(a)) (is_fixed(a[i]) /\ not fix(a[i])) then + not b + elseif is_fixed(b) then % frequent case + if fix(b) = false then + sum (i in index_set(a)) (bool2int(a[i])) <= length(a) - 1 + else + forall (i in index_set(a)) (a[i]) + endif + else + let { + var int: x = bool2int(b); + array [1..length(a)] of var int: c = [bool2int(a[i]) | i in index_set(a)]; + } in length(a) - sum(c) >= 1 - x /\ + % length(a) - sum(c) <= (1 - x) * length(a); % weak + forall (i in index_set(a)) (x <= c[i]) % strong + endif; + +% predicate array_bool_xor(array[int] of var bool: a) = .. sum(a) is odd .. +predicate array_bool_xor( + array [int] of var bool: a, +) = + let { + var 0..(length(a) - 1) div 2: m; + var 1..((length(a) - 1) div 2) * 2 + 1: ss = sum (i in index_set(a)) (bool2int(a[i])); + } in ss == 1 + 2 * m; + +predicate bool_clause( + array [int] of var bool: p :: promise_ctx_monotone, + array [int] of var bool: n :: promise_ctx_antitone, +) = + sum (i in index_set(p)) (bool2int(p[i])) - + sum (i in index_set(n)) (bool2int(n[i])) + + length(n) >= + 1; + +predicate bool_lin_eq(array [int] of int: c, array [int] of var bool: x, var int: d) + :: promise_total = sum (i in index_set(x)) (c[i] * bool2int(x[i])) == d; + +predicate bool_lin_eq_reif( + array [int] of int: c, + array [int] of var bool: x, + int: d, + var bool: b, %% No var int d, sorry +) = aux_int_eq_iff_1(sum (i in index_set(x)) (c[i] * bool2int(x[i])), d, bool2int(b)); diff --git a/minizinc/lib-for-hypercubes/redefs_lin_halfreifs.mzn b/minizinc/lib-for-hypercubes/redefs_lin_halfreifs.mzn new file mode 100644 index 000000000..d8bba0bbd --- /dev/null +++ b/minizinc/lib-for-hypercubes/redefs_lin_halfreifs.mzn @@ -0,0 +1,221 @@ +/* +% FlatZinc built-in redefinitions for linear solvers. +% +% AUTHORS +% Sebastian Brand +% Gleb Belov +*/ + +%-----------------------------------------------------------------------------% +% Auxiliary: indicator constraints +% p -> x # 0 where p is a 0/1 variable and # is a comparison + +% Base cases + +%% used e.g. in element +predicate aux_float_eq_if_1(var float: x, var float: y, var int: p) = + if is_fixed(p) then + if 1 == fix(p) then x == y else true endif + elseif is_fixed(x) /\ is_fixed(y) then %%% Needed to avoid constant domain var + if fix(x) != fix(y) then p == 0 else true endif + elseif is_fixed(x - y) then %%% Hypothetically possible to land here + if 0.0 != fix(x - y) then p == 0 else true endif + elseif fPostprocessDomains /\ fPostproDom_AUX /\ fPostproDom_DIFF then + aux_float_eq_zero_if_1__POST(x - y, p, p) + elseif fMZN__UseIndicators then + aux_float_eq_if_1__IND(x, y, p) + else + aux_float_le_if_1(x, y, p) /\ aux_float_ge_if_1(x, y, p) + endif; + +predicate aux_int_eq_if_1(var int: x, var int: y, var int: p) = + if is_fixed(p) then + if fix(p) = 1 then x = y else true endif + elseif is_fixed(x) /\ is_fixed(y) then + if fix(x) != fix(y) then (p = 0) else true endif + else + % elseif is_fixed(x-y) then % TODO: Necessary for integers?? + % if 0.0!=fix(x-y) then p==0 else true endif + % elseif fPostprocessDomains /\ fPostproDom_AUX /\ fPostproDom_DIFF then + % % TODO MIPDomains + % elseif fMZN__UseIndicators then % TODO: Necessary for integers?? + % aux_float_eq_if_1__IND(x, y, p) + aux_int_le_if_1(x, y, p) /\ aux_int_ge_if_1(x, y, p) + endif; + +predicate aux_float_ne_if_1(var float: x, var float: y, var int: p) = + if is_fixed(p) then + if fix(p) = 1 then (x != y) else true endif + elseif is_fixed(x) /\ is_fixed(y) then %%% Needed to avoid constant domain var + if (fix(x) = fix(y)) then (p = 0) else true endif + elseif is_fixed(x - y) then %%% Hypothetically possible to land here + if (0.0 = fix(x - y)) then (p = 0) else true endif + else + % TODO: What is necessary for not equals? + % elseif fPostprocessDomains /\ fPostproDom_AUX /\ fPostproDom_DIFF then + % aux_float_ne_zero_if_1__POST(x - y, p, p) + % elseif fMZN__UseIndicators then + % aux_float_ne_if_1__IND(x, y, p) + let { array [1..2] of var bool: q } in (q[1] -> (x < y)) /\ (q[2] -> (x > y)) /\ (sum(q) = p) + endif; + +predicate aux_int_ne_if_1(var int: x, var int: y, var int: p) = + if is_fixed(p) then + if fix(p) = 1 then x != y else true endif + elseif is_fixed(x) /\ is_fixed(y) then + if fix(x) = fix(y) then (p = 0) else true endif + else + % elseif is_fixed(x-y) then % TODO: Necessary for integers?? + % if 0.0!=fix(x-y) then p==0 else true endif + % elseif fPostprocessDomains /\ fPostproDom_AUX /\ fPostproDom_DIFF then + % % TODO MIPDomains + % elseif fMZN__UseIndicators then % TODO: Necessary for integers?? + % aux_float_eq_if_1__IND(x, y, p) + let { array [1..2] of var bool: q } in (q[1] -> (x < y)) /\ (q[2] -> (x > y)) /\ (sum(q) = p) + endif; + +predicate aux_int_le_zero_if_0(var int: x, var int: p) = + if is_fixed(p) then + if 0 == fix(p) then x <= 0 else true endif %% 0==fix !! + elseif lb(x) > 0 then + p == 1 + elseif not (0 in dom(x)) then + let { + constraint + assert( + ub(x) < infinity, + "aux_int_le_zero_if_0: variable \(x)'s domain: dom(\(x)) = \( + dom(x) + ), should have finite upper bound\n", + ); + set of int: sDomNeg = dom(x) intersect -infinity..-1; + constraint + assert( + card(sDomNeg) > 0, + "Variable \(x): dom(\(x)) = \(dom(x)), but dom() intersect -inf..-1: \(sDomNeg)\n", + ); + } in aux_int_le_if_0(x, max(sDomNeg), p) + elseif fPostprocessDomains /\ fPostproDom_AUX then + aux_int_le_zero_if_1__POST(x, 1 - p) + elseif fMZN__UseIndicators then + aux_int_le_zero_if_0__IND(x, p) + else + assert( + ub(x) < infinity, + "Variable \(x) needs finite upper bound for a big-M constraint, current domain \(dom(x))", + ) /\ + x <= ub(x) * p + endif; + +predicate aux_float_le_zero_if_0(var float: x, var int: p) = + %% TODO actually only need has_ub(x) in ub(x)*p but lb/ub fail on var float (not on var -infinity..const) 22.2.19 + assert(has_bounds(x), "Variable \(x) needs finite bounds for a big-M constraint") /\ + if is_fixed(p) then + if 0 == fix(p) then x <= 0.0 else true endif + elseif lb(x) > 0.0 then + p == 1 + elseif fPostprocessDomains /\ fPostproDom_AUX then + aux_float_le_zero_if_1__POST(x, 1 - p, 1 - p) + elseif fMZN__UseIndicators then + aux_float_le_zero_if_0__IND(x, p) + else + x <= ub(x) * p + endif; + +predicate aux_float_lt_zero_if_0(var float: x, var int: p) = + assert(has_bounds(x), "Variable \(x) needs finite bounds for a big-M constraint") /\ + if is_fixed(p) then + if 0 == fix(p) then x < 0.0 else true endif + elseif lb(x) >= 0.0 then + p == 1 + elseif fPostprocessDomains /\ fPostproDom_AUX then + aux_float_lt_zero_if_1__POST(x, 1 - p, 1 - p, float_lt_EPS) + elseif fMZN__UseIndicators then + aux_float_le_zero_if_0__IND(x + float_lt_EPS, p) %% Here just absolute EPS, TODO + else + %% let { float: rho = float_lt_EPS_coef__ * max(abs(ub(x)), abs(lb(x))) } % same order of magnitude as ub(x) + let { + float: rho = float_lt_EPS; % absolute eps + } in %%% This one causes 2x- derivation of EPS: + %% x < (ub(x) + rho) * p + %%% Better? + x <= (ub(x) + rho) * p - rho + %%% This just uses absolute eps: + %% x < (ub(x) + float_lt_EPS) * p + endif; + +% Derived cases +predicate aux_int_le_if_0(var int: x, var int: y, var int: p) = aux_int_le_zero_if_0(x - y, p); + +predicate aux_int_ge_if_0(var int: x, var int: y, var int: p) = aux_int_le_zero_if_0(y - x, p); + +predicate aux_int_le_if_1(var int: x, var int: y, var int: p) = aux_int_le_zero_if_0(x - y, 1 - p); + +predicate aux_int_ge_if_1(var int: x, var int: y, var int: p) = aux_int_le_zero_if_0(y - x, 1 - p); + +predicate aux_int_lt_if_0(var int: x, var int: y, var int: p) = aux_int_le_zero_if_0(x - y + 1, p); + +predicate aux_int_gt_if_0(var int: x, var int: y, var int: p) = aux_int_le_zero_if_0(y - x + 1, p); + +predicate aux_int_lt_if_1(var int: x, var int: y, var int: p) = + aux_int_le_zero_if_0(x - y + 1, 1 - p); + +predicate aux_int_gt_if_1(var int: x, var int: y, var int: p) = + aux_int_le_zero_if_0(y - x + 1, 1 - p); + +%% int: switching differences to float to avoid creating integer vars +/* Used anywhere? +predicate aux_int_le_if_0(var float: x, var float: y, var int: p) = + aux_float_le_zero_if_0(x - y, p); + +predicate aux_int_ge_if_0(var float: x, var float: y, var int: p) = + aux_float_le_zero_if_0(y - x, p); + +predicate aux_int_le_if_1(var float: x, var float: y, var int: p) = + aux_float_le_zero_if_0(x - y, 1 - p); + +predicate aux_int_ge_if_1(var float: x, var float: y, var int: p) = + aux_float_le_zero_if_0(y - x, 1 - p); + +predicate aux_int_lt_if_0(var float: x, var float: y, var int: p) = + aux_float_le_zero_if_0(x - y + 1.0, p); + +predicate aux_int_gt_if_0(var float: x, var float: y, var int: p) = + aux_float_le_zero_if_0(y - x + 1.0, p); + +predicate aux_int_lt_if_1(var float: x, float: y, var int: p) = + aux_float_le_zero_if_0(x - y + 1.0, 1 - p); +*/ + +predicate aux_float_le_if_0(var float: x, var float: y, var int: p) = + aux_float_le_zero_if_0(x - y, p); + +predicate aux_float_ge_if_0(var float: x, var float: y, var int: p) = + aux_float_le_zero_if_0(y - x, p); + +predicate aux_float_le_if_1(var float: x, var float: y, var int: p) = + aux_float_le_zero_if_0(x - y, 1 - p); + +predicate aux_float_ge_if_1(var float: x, var float: y, var int: p) = + aux_float_le_zero_if_0(y - x, 1 - p); + +predicate aux_float_lt_if_0(var float: x, var float: y, var int: p) = + aux_float_lt_zero_if_0(x - y, p); + +predicate aux_float_gt_if_0(var float: x, var float: y, var int: p) = + aux_float_lt_zero_if_0(y - x, p); + +predicate aux_float_lt_if_1(var float: x, var float: y, var int: p) = + aux_float_lt_zero_if_0(x - y, 1 - p); + +predicate aux_float_gt_if_1(var float: x, var float: y, var int: p) = + aux_float_lt_zero_if_0(y - x, 1 - p); + +% -------------------------- Domains postpro --------------------------- +%% To avoid looking if an original int var x-y exists and has eq_encode: +%% Passing both int and float version of the indicator for flexibility: +predicate aux_float_eq_zero_if_1__POST(var float: x, var int: pI, var float: p); + +predicate aux_int_le_zero_if_1__POST(var int: x, var int: p); +predicate aux_float_le_zero_if_1__POST(var float: x, var int: pI, var float: p); +predicate aux_float_lt_zero_if_1__POST(var float: x, var int: pI, var float: p, float: eps); diff --git a/minizinc/lib-for-hypercubes/redefs_lin_imp.mzn b/minizinc/lib-for-hypercubes/redefs_lin_imp.mzn new file mode 100644 index 000000000..3f404231a --- /dev/null +++ b/minizinc/lib-for-hypercubes/redefs_lin_imp.mzn @@ -0,0 +1,190 @@ +include "redefs_lin_halfreifs.mzn"; +include "redefs_lin_reifs.mzn"; + +predicate int_lin_le_imp(array [int] of int: c, array [int] of var int: x, int: d, var bool: b); +predicate int_lin_eq_imp(array [int] of int: c, array [int] of var int: x, int: d, var bool: b); + +%% var, var +predicate int_le_imp(var int: x, var int: y, var bool: b) = + if is_fixed(b) then + if fix(b) then x <= y else true endif + elseif ub(x) <= lb(y) then + true + elseif lb(x) > ub(y) then + (not b) + else + aux_int_le_if_1(x, y, b) + endif; + +%% var, var +predicate int_lt_imp(var int: x, var int: y, var bool: b) = + if is_fixed(x) then int_le_imp(x + 1, y, b) else int_le_imp(x, y - 1, b) endif; + +%% var, var +predicate int_eq_imp(var int: x, var int: y, var bool: b) = + if is_fixed(b) then + if fix(b) then (x = y) else true endif + elseif card(dom(x) intersect dom(y)) > 0 then + if is_fixed(x) then + if is_fixed(y) then b -> (fix(x) = fix(y)) else int_eq_imp(y, fix(x), b) endif + elseif is_fixed(y) then + int_eq_imp(x, fix(y), b) + else + %% Simple disjunction + aux_int_eq_if_1(x, y, b) + endif + else + not b + endif; + +%% var, const +predicate int_eq_imp(var int: x, int: y, var bool: b) = + if is_fixed(b) then + if fix(b) then x = y else true endif + elseif y in dom(x) then + if is_fixed(x) then + b -> y = fix(x) + else + aux_int_eq_if_1(x, y, b) %% Simple disjunction + endif + else + not b + endif; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%% NOTE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%% Omitting int_(lin)_ne_imp %%%%%%%%%%%%%%%%%%% +%% so it falls back to _reif, full reification, for performance %% +%% TODO also for floats %% + +%-----------------------------------------------------------------------------% + +% %% lin_expr, const +% predicate int_lin_le_imp(array [int] of int: c, array [int] of var int: x, int: d, var bool: b) = +% if (d = 0) /\ (length(c) = 2) /\ (abs(c[1]) = 1) /\ (c[1] = -1 * c[2]) then +% if (c[1] < 0) then int_le_imp(x[2], x[1], b) else int_le_imp(x[1], x[2], b) endif +% elseif fPostprocessDomains /\ fPostproDom_DIFF then +% int_le_imp(sum (i in index_set(x)) (c[i] * x[i]), d, b) +% elseif fAvoidNI then +% aux_float_le_if_1(sum2float(c, x), d, b) +% else +% aux_int_le_if_1(sum (i in index_set(x)) (c[i] * x[i]), d, b) +% endif; + +predicate int_lin_lt_imp(array [int] of int: c, array [int] of var int: x, int: d, var bool: b) = + if true then + abort("int_lin_lt_imp not supposed to be called") + else + int_lin_le_imp(c, x, d - 1, b) + endif; + +% %% lin_expr, const +% predicate int_lin_eq_imp(array [int] of int: c, array [int] of var int: x, int: d, var bool: b) = +% if (d = 0) /\ (length(c) = 2) /\ (abs(c[1]) = 1) /\ (c[1] = -1 * c[2]) then +% int_eq_imp(x[1], x[2], b) +% elseif fPostprocessDomains /\ fPostproDom_DIFF then +% int_eq_imp(sum (i in index_set(x)) (c[i] * x[i]), d, b) +% elseif fAvoidNI then +% aux_float_eq_if_1(sum2float(c, x), d, b) +% else +% aux_int_eq_if_1(sum (i in index_set(x)) (c[i] * x[i]), d, b) +% endif; + +%-----------------------------------------------------------------------------% + +%% var float, var float +predicate float_le_imp(var float: x, var float: y, var bool: b) = + if is_fixed(b) then + if fix(b) then x <= y else true endif + elseif ub(x) <= lb(y) then + true + elseif lb(x) > ub(y) then + (not b) + else + aux_float_le_if_1(x, y, b) + endif; + +%% var float, var float +predicate float_lt_imp(var float: x, var float: y, var bool: b) = + if is_fixed(b) then if fix(b) then (x < y) else true endif else aux_float_lt_if_1(x, y, b) endif; + +%% var float, var float +predicate float_eq_imp(var float: x, var float: y, var bool: b) = + if is_fixed(b) then + if fix(b) then x = y else true endif + elseif ub(x) < lb(y) \/ lb(x) > ub(y) then + not b + elseif is_fixed(x) /\ is_fixed(y) then + b -> fix(x) == fix(y) + else + aux_float_eq_if_1(x, y, b) + endif; + +%% var float, var float +predicate float_ne_imp(var float: x, var float: y, var bool: b) = + if is_fixed(b) then + if fix(b) then x != y else true endif + elseif ub(x) < lb(y) \/ lb(x) > ub(y) then + true + elseif is_fixed(x) /\ is_fixed(y) then + b -> fix(x) != fix(y) + else + aux_float_ne_if_1(x, y, b) + endif; + +%-----------------------------------------------------------------------------% + +predicate float_lin_eq_imp( + array [int] of float: c, + array [int] of var float: x, + float: d, + var bool: b, +) = + if (d = 0.0) /\ (length(c) = 2) /\ (abs(c[1]) = 1.0) /\ (c[1] = -1.0 * c[2]) then + float_eq_imp(x[1], x[2], b) + elseif fPostprocessDomains /\ fPostproDom_DIFF then + float_eq_imp(sum (i in index_set(x)) (c[i] * x[i]), d, b) + else + aux_float_eq_if_1(sum (i in index_set(x)) (c[i] * x[i]), d, b) + endif; + +predicate float_lin_ne_imp( + array [int] of float: c, + array [int] of var float: x, + float: d, + var bool: b, +) = + if (d = 0.0) /\ (length(c) = 2) /\ (abs(c[1]) = 1.0) /\ (c[1] = -1.0 * c[2]) then + float_ne_imp(x[1], x[2], b) + elseif fPostprocessDomains /\ fPostproDom_DIFF then + float_ne_imp(sum (i in index_set(x)) (c[i] * x[i]), d, not b) + else + aux_float_ne_if_1(sum (i in index_set(x)) (c[i] * x[i]), d, b) + endif; + +predicate float_lin_le_imp( + array [int] of float: c, + array [int] of var float: x, + float: d, + var bool: b, +) = + if (d = 0.0) /\ (length(c) = 2) /\ (abs(c[1]) = 1.0) /\ (c[1] = -1.0 * c[2]) then + if (c[1] < 0.0) then float_le_imp(x[2], x[1], b) else float_le_imp(x[1], x[2], b) endif + elseif fPostprocessDomains /\ fPostproDom_DIFF then + float_le_imp(sum (i in index_set(x)) (c[i] * x[i]), d, b) + else + aux_float_le_if_1(sum (i in index_set(x)) (c[i] * x[i]), d, b) + endif; + +predicate float_lin_lt_imp( + array [int] of float: c, + array [int] of var float: x, + float: d, + var bool: b, +) = + if (d = 0.0) /\ (length(c) = 2) /\ (abs(c[1]) = 1.0) /\ (c[1] = -1.0 * c[2]) then + if (c[1] < 0.0) then float_lt_imp(x[2], x[1], b) else float_lt_imp(x[1], x[2], b) endif + elseif fPostprocessDomains /\ fPostproDom_DIFF then + float_lt_imp(sum (i in index_set(x)) (c[i] * x[i]), d, b) + else + aux_float_lt_if_1(sum (i in index_set(x)) (c[i] * x[i]), d, b) + endif; diff --git a/minizinc/lib-for-hypercubes/redefs_lin_reifs.mzn b/minizinc/lib-for-hypercubes/redefs_lin_reifs.mzn new file mode 100644 index 000000000..57cdd53ea --- /dev/null +++ b/minizinc/lib-for-hypercubes/redefs_lin_reifs.mzn @@ -0,0 +1,484 @@ +predicate int_lin_le_reif(array [int] of int: c, array [int] of var int: x, int: d, var bool: b); +predicate int_lin_eq_reif(array [int] of int: c, array [int] of var int: x, int: d, var bool: b); + +/* +% FlatZinc built-in redefinitions for linear solvers. +% +% AUTHORS +% Sebastian Brand +% Gleb Belov +*/ + +%-----------------------------------------------------------------------------% +% +% Linear equations and inequations +% TODO Use indicators for (half)reifs. +% Otherwise and more using unary encoding for reasonable domains +% +%-----------------------------------------------------------------------------% + +% Domains: reduce all to aux_ stuff? +%% never use Concert's reif feature + +%% var, var +predicate int_le_reif(var int: x, var int: y, var bool: b) = + %% trace(" int_le_reif VV: \(x), \(y), \(b) \n") /\ + if is_fixed(b) then + if true == fix(b) then x <= y else x > y endif + elseif ub(x) <= lb(y) then + b == true + elseif lb(x) > ub(y) then + b == false + elseif fPostprocessDomains /\ fPostproDom_DIFF then + int_le_reif__POST(x - y, 0, b) + else + int_le_reif__NOPOST(x, y, b) + endif; + +%% const, var +predicate int_le_reif(int: x, var int: y, var bool: b) = + %% trace(" int_le_reif CV: \(x), \(y), \(b) \n") /\ + if is_fixed(b) then + if true == fix(b) then x <= y else x > y endif + elseif ub(x) <= lb(y) then + b == true + elseif lb(x) > ub(y) then + b == false + elseif not (x in dom(y)) then %% dom(y) has holes + let { + set of int: sDom2 = dom(y) intersect x + 1..infinity; + constraint + assert( + card(sDom2) > 0, + "Variable: dom(\(y)) = \(dom(y)), but dom() intersect \(x)..inf: \(sDom2)\n", + ); + } in b <-> min(sDom2) <= y + elseif fPostprocessDomains then + int_ge_reif__POST(y, x, b) + else + int_le_reif(-y, -x, b) + endif; + +%% var, const +predicate int_le_reif(var int: x, int: y, var bool: b) = + %% trace(" int_le_reif VC: \(x), \(y), \(b) \n") /\ + if is_fixed(b) then + if true == fix(b) then x <= y else x > y endif + elseif ub(x) <= lb(y) then + b == true + elseif lb(x) > ub(y) then + b == false + elseif not (y in dom(x)) then %% dom(x) has holes + let { + set of int: sDom2 = dom(x) intersect -infinity..y - 1; + constraint + assert( + card(sDom2) > 0, + "Variable: dom(\(x)) = \(dom(x)), but dom() intersect -inf..\(y): \(sDom2)\n", + ); + } in b <-> x <= max(sDom2) + else + if fPostprocessDomains then int_le_reif__POST(x, y, b) else int_le_reif__NOPOST(x, y, b) endif + endif; + +%% var int, var int +predicate int_le_reif__NOPOST( + var int: x, + var int: y, + var bool: b, +) = + aux_int_le_if_1(x, y, b) /\ %% This can call POSTs... TODO + aux_int_gt_if_0(x, y, b); + +%% var, var +predicate int_lt_reif(var int: x, var int: y, var bool: b) = + %% int_le_reif(x-y, -1, b); %% This would produce a new variable x-y and possibly POST it + %% but it looks like differences should not be + if is_fixed(x) then int_le_reif(x + 1, y, b) else int_le_reif(x, y - 1, b) endif; + +%% var, var +predicate int_ne(var int: x, var int: y) = + if fPostproDom_DIFF then int_ne(x - y, 0) else int_ne__SIMPLE(x - y, 0) endif; + +%% var, const +predicate int_ne(var int: x, int: y) = + if y in dom(x) then + if y == ub(x) then + x <= y - 1 + elseif y == lb(x) then + x >= y + 1 + elseif fPostprocessDomains then + int_ne__POST(x, y) + elseif card(dom(x)) < nMZN__UnaryLenMax_neq then + let { array [int] of var int: p = eq_encode(x) } in p[y] == false + else + int_ne__SIMPLE(x, y) + endif + else + true + endif; + +%% var, const. No postprocessing +predicate int_ne__SIMPLE(var int: x, int: y) = + if true then + let { var 0..1: p } in aux_int_lt_if_1(x, y, p) /\ aux_int_gt_if_0(x, y, p) + else + % TODO: This is automatically half-reified, but would still use more + % variables + (x < y) + (x > y) > 0 + endif; + +%% var, var +predicate int_eq_reif(var int: x, var int: y, var bool: b) = + %% trace(" int_eq_reif VV: \(x), \(y), \(b) \n") /\ + if is_fixed(b) then + if fix(b) then x == y else x != y endif + elseif card(dom(x) intersect dom(y)) > 0 then + if is_fixed(x) then + if is_fixed(y) then b <-> fix(x) == fix(y) else int_eq_reif(y, fix(x), b) endif + elseif is_fixed(y) then + int_eq_reif(x, fix(y), b) + elseif fPostprocessDomains /\ fPostproDom_DIFF then + int_eq_reif(x - y, 0, b) + else + aux_int_eq_iff_1(x, y, b) + endif + else + not b + endif; + +%% var, const +predicate int_eq_reif(var int: x, int: y, var bool: b) = + %% trace(" int_eq_reif VC: \(x), \(y), \(b) \n") /\ + if is_fixed(b) then + if fix(b) then x == y else x != y endif + elseif y in dom(x) then + if is_fixed(x) then + b <-> y == fix(x) + elseif card(dom(x)) == 2 then + x == max(dom(x) diff {y}) + b * (y - max(dom(x) diff {y})) %% This should directly connect b to var 0..1: x + elseif fPostprocessDomains then + int_eq_reif__POST(x, y, b) + elseif %%% THIS seems pretty complex, especially for binaries, and does not connect to eq_encoding (/ MIPD?) + %% elseif y==lb(x) then + %% int_lt_reif(y, x, not b) + %% elseif y==ub(x) then + %% int_lt_reif(x, y, not b) + card(dom(x)) < nMZN__UnaryLenMax_eq then + let { array [int] of var int: p = eq_encode(x) } in p[y] == b + else + aux_int_eq_iff_1(x, y, bool2int(b)) + endif + else + b == false + endif; + +predicate int_ne_reif(var int: x, var int: y, var bool: b) = int_eq_reif(x, y, not b); + +predicate int_ne_reif(var int: x, int: y, var bool: b) = int_eq_reif(x, y, not b); + +%-----------------------------------------------------------------------------% + +%% To avoid creating a new int var = sum... +function var float: sum2float(array [int] of int: c, array [int] of var int: x) = + let { + array [int] of float: cF = c; + array [int] of var float: xF = x; + } in sum (i in index_set(xF)) (cF[i] * xF[i]); + +% %% lin_expr, const +% predicate int_lin_le_reif(array [int] of int: c, array [int] of var int: x, int: d, var bool: b) = +% if fPostprocessDomains /\ fPostproDom_DIFF then +% int_le_reif(sum (i in index_set(x)) (c[i] * x[i]), d, b) +% elseif fAvoidNI then +% aux_float_le_if_1(sum2float(c, x), d, b) /\ aux_float_ge_if_0(sum2float(c, x), d + 1, b) +% else +% int_le_reif__NOPOST(sum (i in index_set(x)) (c[i] * x[i]), d, b) +% endif; + +predicate int_lin_lt_reif(array [int] of int: c, array [int] of var int: x, int: d, var bool: b) = + if true then + abort("int_lin_lt_reif not supposed to be called") + else + int_lin_le_reif(c, x, d - 1, b) + endif; + +% %% lin_expr, const +% predicate int_lin_eq_reif(array [int] of int: c, array [int] of var int: x, int: d, var bool: b) = +% if fPostprocessDomains /\ fPostproDom_DIFF then +% int_eq_reif(sum (i in index_set(x)) (c[i] * x[i]), d, b) +% elseif fAvoidNI /\ fAuxIntEqOLD00 then +% aux_int_eq_iff_1__float(sum2float(c, x), d, b) +% else +% aux_int_eq_iff_1(sum (i in index_set(x)) (c[i] * x[i]), d, b) +% endif; + +%% lin_expr, const +predicate int_lin_ne(array [int] of int: c, array [int] of var int: x, int: d) = + if fPostprocessDomains /\ fPostproDom_DIFF then + int_ne(sum (i in index_set(x)) (c[i] * x[i]), d) + elseif fAvoidNI then + int_lin_eq_reif(c, x, d, false) + else + int_ne__SIMPLE(sum (i in index_set(x)) (c[i] * x[i]), d) + endif; + +%% lin_expr, const +predicate int_lin_ne_reif(array [int] of int: c, array [int] of var int: x, int: d, var bool: b) = + if fPostprocessDomains /\ fPostproDom_DIFF then + int_ne_reif(sum (i in index_set(x)) (c[i] * x[i]), d, b) + else + int_lin_eq_reif(c, x, d, not (b)) + endif; + +%-----------------------------------------------------------------------------% + +%% var float, const +predicate float_le_reif(var float: x, float: y, var bool: b) = + %% trace( "float_le_reif(\(x), \(y), \(b))\n" ) /\ + if is_fixed(b) then + if true == fix(b) then x <= y else x > y endif + elseif false then + float_lin_le_reif__IND([1.0, -1.0], [x, y], 0.0, b) + elseif ub(x) <= y then + b == true + elseif lb(x) > y then + b == false + elseif fPostprocessDomains then + float_le_reif__POST(x, y, b, float_lt_EPS) + else + float_le_reif__NOPOST(x, y, b) + endif; + +%% const, var float +predicate float_le_reif(float: x, var float: y, var bool: b) = + if is_fixed(b) then + if true == fix(b) then x <= y else x > y endif + elseif false then + float_lin_le_reif__IND([1.0, -1.0], [x, y], 0.0, b) + elseif ub(x) <= lb(y) then + b == true + elseif lb(x) > ub(y) then + b == false + elseif fPostprocessDomains then + float_ge_reif__POST(y, x, b, float_lt_EPS) + else + float_le_reif(-y, -x, b) + endif; + +%% var float, var float +predicate float_le_reif(var float: x, var float: y, var bool: b) = + if is_fixed(b) then + if true == fix(b) then x <= y else x > y endif + elseif ub(x) <= lb(y) then + b == true + elseif lb(x) > ub(y) then + b == false + elseif fPostprocessDomains /\ fPostproDom_DIFF then + float_le_reif(x - y, 0.0, b) + else + float_le_reif__NOPOST(x - y, 0, b) + endif; + +%% var float, var float +predicate float_le_reif__NOPOST(var float: x, var float: y, var bool: b) = + aux_float_le_if_1(x, y, (b)) /\ %% Can call __POSTs TODO + aux_float_gt_if_0(x, y, (b)); + +%% TODO +predicate float_lt_reif(var float: x, var float: y, var bool: b) = + %% Actually = float_le_reif(x, y-eps, b). + if is_fixed(b) then + if true == fix(b) then x < y else x >= y endif + elseif fPostprocessDomains /\ fPostproDom_DIFF then + aux_float_lt_zero_iff_1__POST(x - y, b, float_lt_EPS) + else + aux_float_lt_if_1(x, y, (b)) /\ aux_float_ge_if_0(x, y, (b)) + endif; + +%% var, const +predicate float_ne(var float: x, float: y) = + if fPostprocessDomains then float_ne__POST(x, y, float_lt_EPS) else float_ne__SIMPLE(x, y) endif; + +predicate float_ne__SIMPLE(var float: x, var float: y) = + if true then + let { var 0..1: p } in aux_float_lt_if_1(x, y, (p)) /\ aux_float_gt_if_0(x, y, (p)) + else + %TODO: Why is this not half-reified? + 1 == (x > y) + (x < y) + endif; + +%% var, var +predicate float_ne(var float: x, var float: y) = + if fPostprocessDomains /\ fPostproDom_DIFF then + float_ne(x - y, 0.0) + else + float_ne__SIMPLE(x - y, 0.0) + endif; + +%% TODO Why is disequality with EPS but equality not?? + +%% var, var? TODO +predicate float_eq_reif(var float: x, var float: y, var bool: b) = + if is_fixed(b) then + if true == fix(b) then x == y else x != y endif + elseif ub(x) < lb(y) \/ lb(x) > ub(y) then + b == false + elseif is_fixed(x) /\ is_fixed(y) then + b == (fix(x) == fix(y)) + elseif fPostprocessDomains /\ fPostproDom_DIFF then + float_eq_reif__POST(x - y, 0, b, float_lt_EPS) + else + aux_float_eq_iff_1(x, y, bool2int(b)) + endif; + +predicate float_ne_reif(var float: x, var float: y, var bool: b) = float_eq_reif(x, y, not (b)); + +%-----------------------------------------------------------------------------% + +predicate float_lin_eq_reif( + array [int] of float: c, + array [int] of var float: x, + float: d, + var bool: b, +) = + if fPostprocessDomains /\ fPostproDom_DIFF then + float_eq_reif(sum (i in index_set(x)) (c[i] * x[i]), d, b) + else + aux_float_eq_iff_1(sum (i in index_set(x)) (c[i] * x[i]), d, b) + endif; + +predicate float_lin_ne_reif( + array [int] of float: c, + array [int] of var float: x, + float: d, + var bool: b, +) = + if fPostprocessDomains /\ fPostproDom_DIFF then + float_ne_reif(sum (i in index_set(x)) (c[i] * x[i]), d, not b) + else + aux_float_eq_iff_1(sum (i in index_set(x)) (c[i] * x[i]), d, not b) + endif; + +predicate float_lin_le_reif( + array [int] of float: c, + array [int] of var float: x, + float: d, + var bool: b, +) = + if fPostprocessDomains /\ fPostproDom_DIFF then + float_le_reif(sum (i in index_set(x)) (c[i] * x[i]), d, b) + else + float_le_reif__NOPOST(sum (i in index_set(x)) (c[i] * x[i]), d, b) + endif; + +predicate float_lin_lt_reif( + array [int] of float: c, + array [int] of var float: x, + float: d, + var bool: b, +) = float_lt_reif(sum (i in index_set(x)) (c[i] * x[i]), d, b); + +%-----------------------------------------------------------------------------% +% Auxiliary: equality reified onto a 0/1 variable + +predicate aux_int_eq_iff_1(var int: x, var int: y, var int: p) = + if is_fixed(p) then + if 1 == fix(p) then x == y else x != y endif + elseif fPostprocessDomains /\ fPostproDom_DIFF then + abort(" aux_int_eq_iff_1 should not be used with full domain postprocessing") + elseif false then + true + elseif fAuxIntEqOLD00 then + let { + array [1..2] of var 0..1: q; + } in aux_int_le_if_1(x, y, p) /\ + aux_int_ge_if_1(x, y, p) /\ + aux_int_lt_if_0(x, y, q[1]) /\ + aux_int_gt_if_0(x, y, q[2]) /\ + sum(q) == p + 1 + else + %% redundant p == (x<=y /\ y<=x) /\ + 1 + p == (x <= y) + (y <= x) + endif; + +predicate aux_int_eq_iff_1__float(var float: x, float: y, var int: p) = + if fAuxIntEqOLD00 then + assert(false, "Don't use aux_int_eq_iff_1__float") + else + /* let { array[1..2] of var 0..1: q } + in + aux_int_le_if_1(x, y, p) /\ + aux_int_ge_if_1(x, y, p) /\ + aux_int_lt_if_0(x, y, q[1]) /\ + aux_int_gt_if_0(x, y, q[2]) /\ + sum(q) == p + 1 */ + assert(false, "Don't use aux_int_eq_iff_1__float with fAuxIntEqOLD00") + endif; + +% Alternative 2 +predicate aux_int_eq_iff_1__WEAK1(var int: x, var int: y, var int: p) = + let { + array [1..2] of var 0..1: q_458; + } in aux_int_lt_if_0(x - p, y, q_458[1]) /\ + aux_int_gt_if_0(x + p, y, q_458[2]) /\ + sum(q_458) <= 2 - 2 * p /\ + sum(q_458) <= 1 + p; + +% Alternative 1 +predicate alt_1_aux_int_eq_iff_1(var int: x, var int: y, var int: p) = + let { + array [1..2] of var 0..1: q; + } in aux_int_lt_if_0(x - p, y, q[1]) /\ + aux_int_gt_if_0(x + p, y, q[2]) /\ + q[1] <= 1 - p /\ + q[2] <= 1 - p /\ + sum(q) <= 1 + p; + +predicate aux_float_eq_iff_1(var float: x, var float: y, var int: p) = + if is_fixed(p) then + if 1 == fix(p) then x == y else x != y endif + elseif fPostprocessDomains /\ fPostproDom_DIFF then + abort(" aux_float_eq_iff_1 should not be used with full domain postprocessing") + elseif fAuxFloatEqOLD00 then + let { + array [1..2] of var 0..1: q; + } in aux_float_le_if_1(x, y, p) /\ + aux_float_ge_if_1(x, y, p) /\ + aux_float_lt_if_0(x, y, (q[1])) /\ + aux_float_gt_if_0(x, y, (q[2])) /\ + sum (i in 1..2) ((q[i])) == 1 + p + else + %% redundant p == (x<=y /\ y<=x) /\ + 1 + p == (x <= y) + (y <= x) + endif; + +% ----------------------------- Domains postpro --------------------------- + +%%%%%%%%%%%%%%%%%% predicate int_le_reif__POST(var int: x, var int: y, var int: b); +%%%%%%%%%%%%%%%%%% predicate int_le_reif__POST(int: x, var int: y, var int: b); +%%%%%%% var int: b: bool2int is a reverse_map, not passed to .fzn +%% var, const +predicate int_le_reif__POST(var int: x, int: y, var int: b); +%% var, const +predicate int_ge_reif__POST(var int: x, int: y, var int: b); + +%% var, const +predicate int_eq_reif__POST(var int: x, int: y, var int: b); +%% var, const +predicate int_ne__POST(var int: x, int: y); + +%%%%%%%%%%%%%%%%%% predicate float_le_reif__POST(var float: x, var float: y, var int: b); +%%%%%%%%%%%%%%%%%% predicate float_le_reif__POST(float: x, var float: y, var int: b); +%% var, const +predicate float_le_reif__POST(var float: x, float: y, var int: b, float: epsRel); +%% var, const +predicate float_ge_reif__POST(var float: x, float: y, var int: b, float: epsRel); + +%% var, var +predicate aux_float_lt_zero_iff_1__POST(var float: x, var int: b, float: epsRel); + +%% var, const +predicate float_eq_reif__POST(var float: x, float: y, var int: b, float: epsRel); +%% var, const +predicate float_ne__POST(var float: x, float: y, float: epsRel); diff --git a/minizinc/lib-for-hypercubes/subcircuit_wDummy.mzn b/minizinc/lib-for-hypercubes/subcircuit_wDummy.mzn new file mode 100644 index 000000000..69f286b8f --- /dev/null +++ b/minizinc/lib-for-hypercubes/subcircuit_wDummy.mzn @@ -0,0 +1,74 @@ +/* Linearized version + * Uses a predicate which constructs a subcircuit which always includes an extra dummy vertex + * Is worse than the just slightly adapted standard variant... +*/ +include "alldifferent.mzn"; + +%% Linear version +predicate subcircuit( + array [int] of var int: x, +) = + let { + set of int: S = index_set(x); + int: l = min(S); + int: u = max(S); + int: n = card(S); + constraint forall (i in S) (x[i] in l..u); + array [l..u + 1] of var l..u + 1: xx; + constraint forall (i in S) (xx[i] in dom(x[i]) union {u + 1}); + } in alldifferent(x) /\ + subcircuit_wDummy(xx) /\ + forall ( + i in S, + j in dom(x[i]), %% also when i==j? + ) ( + eq_encode(x[i])[j] >= + 2 * eq_encode(xx[i])[j] + eq_encode(xx[i])[u + 1] + eq_encode(xx[u + 1])[j] - 1 /\ %% -1 + eq_encode(x[i])[j] >= eq_encode(xx[i])[j] /\ + eq_encode(x[i])[j] <= eq_encode(xx[i])[j] + eq_encode(xx[i])[u + 1] /\ + eq_encode(x[i])[j] <= eq_encode(xx[i])[j] + eq_encode(xx[u + 1])[j] + ) /\ + forall (i in S) (eq_encode(x[i])[i] == eq_encode(xx[i])[i]); + +%% Should include at least 2 nodes if >0? +%% xx[n] is dummy +predicate subcircuit_wDummy( + array [int] of var int: x, +) = + let { + set of int: S = index_set(x); + int: l = min(S); + int: u = max(S); + int: n = card(S); + set of int: S__ = S diff {u}; %% the real nodes + array [S__] of var 2..n: order; + } in %% constraint order[n]==1, %% fix the dummy + %% var bool: empty = (firstin == u+1), no, break 2-cycles with dummy + alldifferent(x) /\ + % NO alldifferent(order) /\ + %%% MTZ model. Note that INTEGER order vars seem better!: + forall (i in S__, j in dom(x[i]) where i != j /\ j != u) ( + order[i] - order[j] + (n - 1) * eq_encode(x[i])[j] <= + % + (n-3)*bool2int(x[j]==i) %% the Desrochers & Laporte '91 term + % --- strangely enough it is much worse on vrp-s2-v2-c7_vrp-v2-c7_det_ADAPT_1_INVERSE.mzn! + n - 2 + ) /\ + %% Break 2-cycles with dummy: + forall ( + i in S__, + ) ( + eq_encode(x[i])[u] + eq_encode(x[u])[i] <= 1 /\ + %% Ensure dummy is in: + if i in dom(x[i]) then eq_encode(x[i])[i] >= eq_encode(x[u])[u] else true endif + ) /\ + % Symmetry? Each node that is not in is numbered after the lastin node. + forall (i in S) ( + true + % (not ins[i]) <-> (n == order[i]) + ); + +predicate subcircuit_reif(array [int] of var int: x, var bool: b) = + abort("Reified subcircuit/1 is not supported."); + +%-----------------------------------------------------------------------------% +%-----------------------------------------------------------------------------% diff --git a/minizinc/pumpkin-for-hypercubes.msc b/minizinc/pumpkin-for-hypercubes.msc new file mode 100644 index 000000000..4ad10a350 --- /dev/null +++ b/minizinc/pumpkin-for-hypercubes.msc @@ -0,0 +1,97 @@ +{ + "name": "Pumpkin", + "id": "nl.tudelft.algorithmics.pumpkin", + "version": "0.1", + "executable": "../target/release/pumpkin-solver", + "mznlib": "./lib-for-hypercubes/", + "stdFlags": ["-v", "-f", "-r", "-t", "-s", "-a"], + + "extraFlags": [ + [ + "--conflict-resolver", + "The conflict resolver to use.", + "string", + "", + ], + [ + "--learning-max-num-clauses", + "\tThe number of high lbd learned clauses that are kept in the database. \n\n\tLearned clauses are kept based on the tiered system introduced in 'Improving SAT Solvers by Exploiting Empirical Characteristics of CDCL - Chanseok Oh (2016)'", + "int:0:18446744073709551615", + "4000" + ], + [ + "--learning-lbd-threshold", + "\tLearned clauses with this threshold LBD or lower are kept permanently.\n\n\tLearned clauses are kept based on the tiered system introduced 'Improving SAT Solvers by Exploiting Empirical Characteristics of CDCL - Chanseok Oh (2016)'", + "int:0:4294967295", + "5" + ], + [ + "--learning-sorting-strategy", + "\tDecides which clauses will be removed when cleaning up the learned clauses.\n\n\tCan either be based on the LBD of a clause (the number of different decision levels) or on the activity of a clause (how often it is used in conflict analysis).\n\n\tPossible values: ['lbd', 'activity']", + "string", + "activity" + ], + [ + "--no-learning-minimise", + "\tDecides whether learned clauses are minimised as a post-processing step after computing the 1-UIP Minimisation is done.\n\n\tThis is done according to the idea proposed in 'Generalized Conflict-Clause Strengthening for Satisfiability Solvers - Allen van Gelder (2011)'." + "bool", + "false", + ], + [ + "--restart-sequence", + "\tDecides the sequence based on which the restarts are performed.\n\t- The 'constant' approach uses a constant number of conflicts before another restart is triggered\n\t- The 'geometric' approach uses a geometrically increasing sequence\n\t- The 'luby' approach uses a recursive sequence of the form 1, 1, 2, 1, 1, 2, 4, 1, 1, 2, 1, 1, 2, 4, 8, 1, 1, 2.... (see 'Optimal speedup of Las Vegas algorithms - Luby et al.(1993)')\n\n\tTo be used in combination with '--restarts-base-interval'.\n\n\tPossible values: ['constant', 'geometric', 'luby']", + "string", + "constant" + ], + [ + "--restart-base-interval", + "\tThe base interval length is used as a multiplier to the restart sequence.\n\t- In the case of the 'constant' restart sequence this argument indicates the constant which is used to determine when a restart occurs\n\t- For the 'geometric' approach this argument indicates the starting value of the sequence\n\t- For the 'luby' approach, the sequence is multiplied by this value\n\n\tFor example, constant restarts with base interval 50 means a restart is triggered every 50 conflicts.", + "int:0:18446744073709551615", + "50", + ] + [ + "--restart-min-initial-conflicts", + "\tIndicates the minimum number of initial conflicts before the first restart can occur.\n\tThis allows the solver to learn some things about the problem before a restart is allowed to occur.", + "int:0:18446744073709551615", + "10000" + ], + [ + "--restart-lbd-coef", + "\tUsed to determine if a restart should be forced (see 'Refining Restarts Strategies for SAT and UNSAT - Audemard and Simon (2012)').\n\n\tThe state is 'bad' if the current LBD value is much greater than the global LBD average.\n\tA greater (lower) value for lbd-coef means a less (more) frequent restart policy.\n\n\tIf the long-term average LBD multiplied by this coefficient is lower than the short-term average LBD then a restart is performed.", + "float", + "1.25" + ] + [ + "--restart-num-assigned-coef", + "\tUsed to determine if a restart should be blocked (see 'Refining Restarts Strategies for SAT and UNSAT - Audemard and Simon (2012)').\n\n\tTo be used in combination with '--restarts-num-assigned-window'.\n\n\tA restart is blocked if the number of assigned propositional variables is much greater than the average number of assigned variables in the recent past.\n\tA greater (lower) value for '--restart-num-assigned-coef' means fewer (more) blocked restarts.", + "float", + "1.4", + ], + [ + "--restart-num-assigned-window", + "\tUsed to determine the length of the recent past that should be considered when deciding on blocking restarts (see 'Refining Restarts Strategies for SAT and UNSAT - Audemard and Simon (2012)').\n\n\tThe solver considers the last '--restart_num_assigned_window' conflicts as the referencepoint for the number of assigned variables.", + "int:0:18446744073709551615", + "5000" + ], + [ + "--restart-geometric-coef", + "\tThe coefficient in the geometric sequence\n\t\t`x_i = x_{i-1} * '--restart-geometric-coef'`\n\t where\n\t\t`x_1 = '--restarts-base-interval'`\n\n\tUsed only if '--restarts-sequence-generator'is assigned to 'geometric'.", + "float", + "1.0" + ], + [ + "--cumulative-allow-holes", + "\tWhether to allow the cumulative propagator(s) to create holes in the domain rather than only propagating the bounds", + "bool", + "false" + ], + [ + "--proof-path", + "The path to the proof file.", + "string", + "", + ] + ], + + "tags": ["cp", "lcg", "int"] +} diff --git a/minizinc/pumpkin.msc b/minizinc/pumpkin.msc index a6c249bb8..6a4e4fbd0 100644 --- a/minizinc/pumpkin.msc +++ b/minizinc/pumpkin.msc @@ -7,6 +7,12 @@ "stdFlags": ["-v", "-f", "-r", "-t", "-s", "-a"], "extraFlags": [ + [ + "--conflict-resolver", + "The conflict resolver to use.", + "string", + "", + ], [ "--learning-max-num-clauses", "\tThe number of high lbd learned clauses that are kept in the database. \n\n\tLearned clauses are kept based on the tiered system introduced in 'Improving SAT Solvers by Exploiting Empirical Characteristics of CDCL - Chanseok Oh (2016)'", diff --git a/pumpkin-checker/src/deductions.rs b/pumpkin-checker/src/deductions.rs index eafe740b3..5ad15a316 100644 --- a/pumpkin-checker/src/deductions.rs +++ b/pumpkin-checker/src/deductions.rs @@ -3,10 +3,10 @@ use std::rc::Rc; use drcp_format::ConstraintId; use drcp_format::IntAtomic; -use pumpkin_checking::AtomicConstraint; -use pumpkin_checking::VariableState; +use pumpkin_checking::SupportingInference; use crate::inferences::Fact; +use crate::model::Atomic; use crate::model::Nogood; /// An inference that was ignored when checking a deduction. @@ -35,10 +35,6 @@ pub enum InvalidDeduction { /// conflict. #[error("no conflict was derived after applying all inferences")] NoConflict(Vec), - - /// The premise contains mutually exclusive atomic constraints. - #[error("the deduction contains inconsistent premises")] - InconsistentPremises, } /// Verify that a deduction is valid given the inferences in the proof stage. @@ -46,212 +42,81 @@ pub fn verify_deduction( deduction: &drcp_format::Deduction, i32>, facts_in_proof_stage: &BTreeMap, ) -> Result { - // To verify a deduction, we assume that the premises are true. Then we go over all the - // facts in the sequence, and if all the premises are satisfied, we apply the consequent. - // At some point, this should either reach a fact without a consequent or derive an - // inconsistent domain. + // First we convert the deduction sequence to the types from the checking library. + let inferences = deduction + .sequence + .iter() + .map(|cid| { + facts_in_proof_stage + .get(cid) + .map(|fact| SupportingInference { + premises: fact.premises.clone(), + consequent: fact.consequent.clone(), + }) + .ok_or(InvalidDeduction::UnknownInference(*cid)) + }) + .collect::, _>>()?; + + // Then we convert the deduction premise to the types from the checking library. + let premises = deduction.premises.iter().cloned().map(Atomic::IntAtomic); + + match pumpkin_checking::verify_deduction(premises.clone(), inferences) { + Ok(_) => Ok(Nogood::from(premises)), + Err(error) => Err(convert_error(error, facts_in_proof_stage)), + } +} - let mut variable_state = VariableState::prepare_for_conflict_check( - deduction.premises.iter().cloned().map(Into::into), - None, - ) - .map_err(|_| InvalidDeduction::InconsistentPremises)?; +fn convert_error( + error: pumpkin_checking::InvalidDeduction, + facts_in_proof_stage: &BTreeMap, +) -> InvalidDeduction { + let pumpkin_checking::InvalidDeduction(ignored_inferences) = error; - let mut unused_inferences = Vec::new(); + let mapped_ignored_inferences = ignored_inferences + .into_iter() + .map(|ignored_inference| { + convert_ignored_inferences(ignored_inference, facts_in_proof_stage) + }) + .collect(); - for constraint_id in deduction.sequence.iter() { - // Get the fact associated with the constraint ID from the sequence. - let fact = facts_in_proof_stage - .get(constraint_id) - .ok_or(InvalidDeduction::UnknownInference(*constraint_id))?; + InvalidDeduction::NoConflict(mapped_ignored_inferences) +} - // Collect all premises that do not evaluate to `true` under the current variable - // state. - let unsatisfied_premises: Vec> = fact - .premises +fn convert_ignored_inferences( + ignored_inference: pumpkin_checking::IgnoredInference, + facts_in_proof_stage: &BTreeMap, +) -> IgnoredInference { + IgnoredInference { + constraint_id: facts_in_proof_stage .iter() - .filter_map::, _>(|premise| { - if variable_state.is_true(premise) { - None + .find_map(|(constraint_id, inference)| { + let constraint_id = *constraint_id; + let checker_inference = inference.clone(); + let ignored_inference_as_checker = Fact::from(ignored_inference.inference.clone()); + + if checker_inference == ignored_inference_as_checker { + Some(constraint_id) } else { - // We need to convert the premise name from a `Rc` to a - // `String`. The former does not implement `Send`, but that is - // required for our error type to be used with anyhow. - Some(IntAtomic { - name: String::from(premise.identifier().as_ref()), - comparison: match premise.comparison() { - pumpkin_checking::Comparison::GreaterEqual => { - drcp_format::IntComparison::GreaterEqual - } - pumpkin_checking::Comparison::LessEqual => { - drcp_format::IntComparison::LessEqual - } - pumpkin_checking::Comparison::Equal => { - drcp_format::IntComparison::Equal - } - pumpkin_checking::Comparison::NotEqual => { - drcp_format::IntComparison::NotEqual - } - }, - value: premise.value(), - }) + None } }) - .collect::>(); - - // If at least one premise is unassigned, this fact is ignored for the conflict - // check and recorded as unused. - if !unsatisfied_premises.is_empty() { - unused_inferences.push(IgnoredInference { - constraint_id: *constraint_id, - unsatisfied_premises, - }); - - continue; - } - - // At this point the premises are satisfied so we handle the consequent of the - // inference. - match &fact.consequent { - Some(consequent) => { - if !variable_state.apply(consequent) { - // If applying the consequent yields an empty domain for a - // variable, then the deduction is valid. - return Ok(Nogood::from(deduction.premises.clone())); - } - } - // If the consequent is explicitly false, then the deduction is valid. - None => return Ok(Nogood::from(deduction.premises.clone())), - } - } - - // Reaching this point means that the conjunction of inferences did not yield to a - // conflict. Therefore the deduction is invalid. - Err(InvalidDeduction::NoConflict(unused_inferences)) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::atomic; - use crate::fact; - use crate::test_utils::constraint_id; - use crate::test_utils::deduction; - - macro_rules! facts { - {$($k: expr => $v: expr),* $(,)?} => { - ::std::collections::BTreeMap::from([$(($crate::test_utils::constraint_id($k), $v),)*]) - }; - } - - #[test] - fn a_sequence_is_correctly_traversed() { - let premises = vec![atomic!([x >= 5])]; - let deduction = deduction(5, premises.clone(), [1, 2, 3]); - - let facts_in_proof_stage = facts! { - 1 => fact!([x >= 5] -> [y <= 4]), - 2 => fact!([y <= 7] -> [z != 10]), - 3 => fact!([y <= 5] & [z != 10] -> [x <= 4]), - }; - - let nogood = verify_deduction(&deduction, &facts_in_proof_stage).expect("valid deduction"); - assert_eq!(Nogood::from(premises), nogood); - } - - #[test] - fn an_inference_implying_false_is_a_valid_stopping_condition() { - let premises = vec![atomic!([x >= 5])]; - let deduction = deduction(5, premises.clone(), [1, 2, 3]); - - let facts_in_proof_stage = facts! { - 1 => fact!([x >= 5] -> [y <= 4]), - 2 => fact!([y <= 7] -> [z != 10]), - 3 => fact!([y <= 5] & [z != 10] -> false), - }; - - let nogood = verify_deduction(&deduction, &facts_in_proof_stage).expect("valid deduction"); - assert_eq!(Nogood::from(premises), nogood); - } - - #[test] - fn inference_order_does_not_need_to_be_by_constraint_id() { - let premises = vec![atomic!([x >= 5])]; - let deduction = deduction(5, premises.clone(), [2, 1, 4]); - - let facts_in_proof_stage = facts! { - 2 => fact!([x >= 5] -> [y <= 4]), - 1 => fact!([y <= 7] -> [z != 10]), - 4 => fact!([y <= 5] & [z != 10] -> false), - }; - - let nogood = verify_deduction(&deduction, &facts_in_proof_stage).expect("valid deduction"); - assert_eq!(Nogood::from(premises), nogood); - } - - #[test] - fn inconsistent_premises_are_identified() { - let premises = vec![atomic!([x >= 5]), atomic!([x <= 4])]; - let deduction = deduction(5, premises.clone(), [2]); - - let facts_in_proof_stage = facts! { - 2 => fact!([x == 5] -> false), - }; - - let error = - verify_deduction(&deduction, &facts_in_proof_stage).expect_err("inconsistent premises"); - assert_eq!(InvalidDeduction::InconsistentPremises, error); - } - - #[test] - fn all_inferences_in_sequence_must_be_in_fact_database() { - let premises = vec![atomic!([x >= 5])]; - let deduction = deduction(5, premises.clone(), [1, 2]); - - let facts_in_proof_stage = facts! { - 2 => fact!([x == 5] -> false), - }; - - let error = - verify_deduction(&deduction, &facts_in_proof_stage).expect_err("unknown inference"); - assert_eq!(InvalidDeduction::UnknownInference(constraint_id(1)), error); - } - - #[test] - fn sequence_that_does_not_terminate_in_conflict_is_rejected() { - let premises = vec![atomic!([x >= 5])]; - let deduction = deduction(5, premises.clone(), [2, 1]); - - let facts_in_proof_stage = facts! { - 2 => fact!([x >= 5] -> [y <= 4]), - 1 => fact!([y <= 7] -> [z != 10]), - 4 => fact!([y <= 5] & [z != 10] -> false), - }; - - let error = - verify_deduction(&deduction, &facts_in_proof_stage).expect_err("unknown inference"); - assert_eq!(InvalidDeduction::NoConflict(vec![]), error); - } - - #[test] - fn inferences_with_unsatisfied_premises_are_ignored() { - let premises = vec![atomic!([x >= 5])]; - let deduction = deduction(5, premises.clone(), [2, 1]); - - let facts_in_proof_stage = facts! { - 2 => fact!([x >= 5] -> [y <= 4]), - 1 => fact!([y <= 7] & [x >= 6] -> [z != 10]), - 4 => fact!([y <= 5] & [z != 10] -> false), - }; - - let error = - verify_deduction(&deduction, &facts_in_proof_stage).expect_err("unknown inference"); - assert_eq!( - InvalidDeduction::NoConflict(vec![IgnoredInference { - constraint_id: constraint_id(1), - unsatisfied_premises: vec![atomic!([x string >= 6])], - }]), - error - ); + .expect("one of these will match"), + + unsatisfied_premises: ignored_inference + .unsatisfied_premises + .into_iter() + .map(|premise| match premise { + Atomic::True | Atomic::False => unreachable!(), + + Atomic::IntAtomic(int_atomic) => IntAtomic { + // Note: String is required here since the error type needs to + // implement `Send`. By default we use `Rc` everywhere, which + // does not implement `Send`. + name: String::from(int_atomic.name.as_ref()), + comparison: int_atomic.comparison, + value: int_atomic.value, + }, + }) + .collect(), } } diff --git a/pumpkin-checker/src/inferences/mod.rs b/pumpkin-checker/src/inferences/mod.rs index f83e5dc39..cc6262f5c 100644 --- a/pumpkin-checker/src/inferences/mod.rs +++ b/pumpkin-checker/src/inferences/mod.rs @@ -4,17 +4,27 @@ mod linear; mod nogood; mod time_table; +use pumpkin_checking::SupportingInference; use pumpkin_checking::VariableState; use crate::model::Atomic; use crate::model::Model; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Fact { pub premises: Vec, pub consequent: Option, } +impl From> for Fact { + fn from(value: SupportingInference) -> Self { + Fact { + premises: value.premises, + consequent: value.consequent, + } + } +} + impl Fact { /// Create a fact `premises -> false`. pub fn nogood(premises: Vec) -> Self { diff --git a/pumpkin-checker/src/test_utils.rs b/pumpkin-checker/src/test_utils.rs index af003e707..b05888c3d 100644 --- a/pumpkin-checker/src/test_utils.rs +++ b/pumpkin-checker/src/test_utils.rs @@ -1,10 +1,8 @@ //! Contains a bunch of utilities to help write tests for the checker. use std::num::NonZero; -use std::rc::Rc; use drcp_format::ConstraintId; -use drcp_format::IntAtomic; /// Create a constraint ID from the given number. /// @@ -90,18 +88,3 @@ macro_rules! fact { } }; } - -pub(crate) fn deduction( - id: u32, - premises: impl Into, i32>>>, - sequence: impl IntoIterator, -) -> drcp_format::Deduction, i32> { - drcp_format::Deduction { - constraint_id: constraint_id(id), - premises: premises.into(), - sequence: sequence - .into_iter() - .map(|id| NonZero::new(id).expect("constraint ids should be non-zero")) - .collect(), - } -} diff --git a/pumpkin-crates/checking/Cargo.toml b/pumpkin-crates/checking/Cargo.toml index 485564df6..2bda5514a 100644 --- a/pumpkin-crates/checking/Cargo.toml +++ b/pumpkin-crates/checking/Cargo.toml @@ -10,6 +10,7 @@ authors.workspace = true [dependencies] dyn-clone = "1.0.20" fnv = "1.0.7" +thiserror = "2.0.18" [lints] workspace = true diff --git a/pumpkin-crates/checking/src/atomic_constraint.rs b/pumpkin-crates/checking/src/atomic_constraint.rs index 6be4a9508..38711717f 100644 --- a/pumpkin-crates/checking/src/atomic_constraint.rs +++ b/pumpkin-crates/checking/src/atomic_constraint.rs @@ -8,7 +8,7 @@ use std::hash::Hash; /// - `identifier` identifies a variable, /// - `op` is a [`Comparison`], /// - and `value` is an integer. -pub trait AtomicConstraint: Sized + Debug { +pub trait AtomicConstraint: Clone + Debug + Sized { /// The type of identifier used for variables. type Identifier: Hash + Eq; @@ -87,3 +87,34 @@ impl AtomicConstraint for TestAtomic { } } } + +/// A convenient way to construct a [`TestAtomic`]. +/// +/// # Example +/// ``` +/// pumpkin_checking::test_atomic!([x >= 5]); +/// pumpkin_checking::test_atomic!([y != 10]); +/// ``` +#[macro_export] +macro_rules! test_atomic { + (@to_comparison >=) => { + $crate::Comparison::GreaterEqual + }; + (@to_comparison <=) => { + $crate::Comparison::LessEqual + }; + (@to_comparison ==) => { + $crate::Comparison::Equal + }; + (@to_comparison !=) => { + $crate::Comparison::NotEqual + }; + + ([$name:ident $comp:tt $value:expr]) => { + $crate::TestAtomic { + name: stringify!($name), + comparison: $crate::test_atomic!(@to_comparison $comp), + value: $value, + } + }; +} diff --git a/pumpkin-crates/checking/src/deduction_checker.rs b/pumpkin-crates/checking/src/deduction_checker.rs new file mode 100644 index 000000000..d0866a31d --- /dev/null +++ b/pumpkin-crates/checking/src/deduction_checker.rs @@ -0,0 +1,208 @@ +use crate::AtomicConstraint; +use crate::VariableState; + +/// An inference that was ignored when checking a deduction. +/// +/// Returned as an error when checking a deduction. These inferences were added to the proof stage, +/// but never used. Hence, they likely point to why the proof stage is rejected. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IgnoredInference { + /// The inference that was ignored. + pub inference: SupportingInference, + + /// The premises that were not satisfied when the inference was evaluated. + pub unsatisfied_premises: Vec, +} + +/// A deduction is rejected by the checker. +/// +/// The inferences in the proof stage do not derive an empty domain or an explicit +/// conflict. +#[derive(thiserror::Error, Debug, PartialEq, Eq)] +#[error("no conflict was derived after applying all inferences")] +pub struct InvalidDeduction(pub Vec>); + +/// An inference used to support a deduction. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SupportingInference { + /// The premises of the inference. + pub premises: Vec, + /// The consequent of the inference. + /// + /// [`None`] represents the literal false. I.e., if the consequent is [`None`], then the + /// premises imply false. + pub consequent: Option, +} + +/// Verify that a deduction is valid given the inferences in the proof stage. +/// +/// The `inferences` are considered in the order they are provided. +pub fn verify_deduction( + premises: impl IntoIterator, + inferences: impl IntoIterator>, +) -> Result<(), InvalidDeduction> +where + Atomic: AtomicConstraint, +{ + // To verify a deduction, we assume that the premises are true. Then we go over all the + // facts in the sequence, and if all the premises are satisfied, we apply the consequent. + // At some point, this should either reach a fact without a consequent or derive an + // inconsistent domain. + + let Ok(mut variable_state) = VariableState::prepare_for_conflict_check(premises, None) else { + // If the deduction contains inconsistent premises, its trivially valid. + return Ok(()); + }; + + let mut unused_inferences = Vec::new(); + + for inference in inferences.into_iter() { + // Collect all premises that do not evaluate to `true` under the current variable + // state. + let unsatisfied_premises = inference + .premises + .iter() + .filter(|premise| !variable_state.is_true(premise)) + .cloned() + .collect::>(); + + // If at least one premise is unassigned, this fact is ignored for the conflict + // check and recorded as unused. + if !unsatisfied_premises.is_empty() { + unused_inferences.push(IgnoredInference { + inference, + unsatisfied_premises, + }); + + continue; + } + + // At this point the premises are satisfied so we handle the consequent of the + // inference. + match &inference.consequent { + Some(consequent) => { + if !variable_state.apply(consequent) { + // If applying the consequent yields an empty domain for a + // variable, then the deduction is valid. + return Ok(()); + } + } + // If the consequent is explicitly false, then the deduction is valid. + None => return Ok(()), + } + } + + // Reaching this point means that the conjunction of inferences did not yield to a + // conflict. Therefore the deduction is invalid. + Err(InvalidDeduction(unused_inferences)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_atomic; + + /// Create a [`SupportingInference`] in a DSL. + /// + /// # Example + /// ``` + /// inference!([x >= 5] & [y <= 10] -> [z == 5]); + /// inference!([x >= 5] & [y <= 10] -> false); + /// ``` + #[macro_export] + macro_rules! inference { + // Case: consequent is an Atomic + ( + $($prem:tt)&+ -> [$($cons:tt)+] + ) => { + SupportingInference { + premises: vec![$( test_atomic!($prem) ),+], + consequent: Some(test_atomic!([$($cons)+])), + } + }; + + // Case: consequent is false (i.e., None) + ( + $($prem:tt)&+ -> false + ) => { + SupportingInference { + premises: vec![$( test_atomic!($prem) ),+], + consequent: None, + } + }; + } + + #[test] + fn a_sequence_is_correctly_traversed() { + let premises = vec![test_atomic!([x >= 5])]; + + let inferences = vec![ + inference!([x >= 5] -> [y <= 4]), + inference!([y <= 7] -> [z != 10]), + inference!([y <= 5] & [z != 10] -> [x <= 4]), + ]; + + verify_deduction(premises, inferences).expect("valid deduction"); + } + + #[test] + fn an_inference_implying_false_is_a_valid_stopping_condition() { + let premises = vec![test_atomic!([x >= 5])]; + + let inferences = vec![ + inference!([x >= 5] -> [y <= 4]), + inference!([y <= 7] -> [z != 10]), + inference!([y <= 5] & [z != 10] -> false), + ]; + + verify_deduction(premises, inferences).expect("valid deduction"); + } + + #[test] + fn inconsistent_premises_are_no_problem() { + let premises = vec![test_atomic!([x >= 5]), test_atomic!([x <= 4])]; + + let inferences = vec![inference!([x == 5] -> false)]; + + verify_deduction(premises, inferences).expect("no inconsistency"); + } + + #[test] + fn sequence_that_does_not_terminate_in_conflict_is_rejected() { + let premises = vec![test_atomic!([x >= 5])]; + + let inferences = vec![ + inference!([x >= 5] -> [y <= 4]), + inference!([y <= 7] -> [z != 10]), + ]; + + let error = verify_deduction(premises, inferences).expect_err("conflict is not reached"); + assert_eq!(InvalidDeduction(vec![]), error); + } + + #[test] + fn inferences_with_unsatisfied_premises_are_ignored() { + let premises = vec![test_atomic!([x >= 5])]; + + let inferences = vec![ + inference!([x >= 5] -> [y <= 4]), + inference!([y <= 7] & [x >= 6] -> [z != 10]), + inference!([y <= 5] & [z != 10] -> false), + ]; + + let error = verify_deduction(premises, inferences).expect_err("premises are not satisfied"); + assert_eq!( + InvalidDeduction(vec![ + IgnoredInference { + inference: inference!([y <= 7] & [x >= 6] -> [z != 10]), + unsatisfied_premises: vec![test_atomic!([x >= 6])], + }, + IgnoredInference { + inference: inference!([y <= 5] & [z != 10] -> false), + unsatisfied_premises: vec![test_atomic!([z != 10])], + } + ]), + error + ); + } +} diff --git a/pumpkin-crates/checking/src/inference_checker.rs b/pumpkin-crates/checking/src/inference_checker.rs new file mode 100644 index 000000000..badb75d8e --- /dev/null +++ b/pumpkin-crates/checking/src/inference_checker.rs @@ -0,0 +1,49 @@ +use std::fmt::Debug; + +use dyn_clone::DynClone; + +use crate::AtomicConstraint; +use crate::VariableState; + +/// An inference checker tests whether the given state is a conflict under the sematics of an +/// inference rule. +pub trait InferenceChecker: Debug + DynClone { + /// Returns `true` if `state` is a conflict, and `false` if not. + /// + /// For the conflict check, all the premises are true in the state and the consequent, if + /// present, is false. + fn check( + &self, + state: VariableState, + premises: &[Atomic], + consequent: Option<&Atomic>, + ) -> bool; +} + +/// Wrapper around `Box>` that implements [`Clone`]. +#[derive(Debug)] +pub struct BoxedChecker(Box>); + +impl Clone for BoxedChecker { + fn clone(&self) -> Self { + BoxedChecker(dyn_clone::clone_box(&*self.0)) + } +} + +impl From>> for BoxedChecker { + fn from(value: Box>) -> Self { + BoxedChecker(value) + } +} + +impl BoxedChecker { + /// See [`InferenceChecker::check`]. + pub fn check( + &self, + variable_state: VariableState, + premises: &[Atomic], + consequent: Option<&Atomic>, + ) -> bool { + self.0.check(variable_state, premises, consequent) + } +} diff --git a/pumpkin-crates/checking/src/lib.rs b/pumpkin-crates/checking/src/lib.rs index 6fb8fc265..88ffd94e9 100644 --- a/pumpkin-crates/checking/src/lib.rs +++ b/pumpkin-crates/checking/src/lib.rs @@ -4,59 +4,17 @@ //! inferences are sound w.r.t. an inference rule. mod atomic_constraint; +mod deduction_checker; +mod inference_checker; mod int_ext; mod union; mod variable; mod variable_state; -use std::fmt::Debug; - pub use atomic_constraint::*; -use dyn_clone::DynClone; +pub use deduction_checker::*; +pub use inference_checker::*; pub use int_ext::*; pub use union::*; pub use variable::*; pub use variable_state::*; - -/// An inference checker tests whether the given state is a conflict under the sematics of an -/// inference rule. -pub trait InferenceChecker: Debug + DynClone { - /// Returns `true` if `state` is a conflict, and `false` if not. - /// - /// For the conflict check, all the premises are true in the state and the consequent, if - /// present, if false. - fn check( - &self, - state: VariableState, - premises: &[Atomic], - consequent: Option<&Atomic>, - ) -> bool; -} - -/// Wrapper around `Box>` that implements [`Clone`]. -#[derive(Debug)] -pub struct BoxedChecker(Box>); - -impl Clone for BoxedChecker { - fn clone(&self) -> Self { - BoxedChecker(dyn_clone::clone_box(&*self.0)) - } -} - -impl From>> for BoxedChecker { - fn from(value: Box>) -> Self { - BoxedChecker(value) - } -} - -impl BoxedChecker { - /// See [`InferenceChecker::check`]. - pub fn check( - &self, - variable_state: VariableState, - premises: &[Atomic], - consequent: Option<&Atomic>, - ) -> bool { - self.0.check(variable_state, premises, consequent) - } -} diff --git a/pumpkin-crates/checking/src/variable_state.rs b/pumpkin-crates/checking/src/variable_state.rs index 07cb1d253..02c2b3d64 100644 --- a/pumpkin-crates/checking/src/variable_state.rs +++ b/pumpkin-crates/checking/src/variable_state.rs @@ -14,7 +14,7 @@ use crate::IntExt; /// /// Domains can be reduced through [`VariableState::apply`]. By default, the domain of every /// variable is infinite. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct VariableState { domains: FnvHashMap, } @@ -226,7 +226,7 @@ where } /// A domain inside the variable state. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Domain { lower_bound: IntExt, upper_bound: IntExt, diff --git a/pumpkin-crates/conflict-resolvers/src/minimisers/recursive_minimiser.rs b/pumpkin-crates/conflict-resolvers/src/minimisers/recursive_minimiser.rs index 871482f20..834e2268f 100644 --- a/pumpkin-crates/conflict-resolvers/src/minimisers/recursive_minimiser.rs +++ b/pumpkin-crates/conflict-resolvers/src/minimisers/recursive_minimiser.rs @@ -1,3 +1,4 @@ +use pumpkin_core::asserts::pumpkin_assert_eq_simple; use pumpkin_core::asserts::pumpkin_assert_moderate; use pumpkin_core::asserts::pumpkin_assert_simple; use pumpkin_core::conflict_resolving::ConflictAnalysisContext; @@ -7,6 +8,7 @@ use pumpkin_core::create_statistics_struct; use pumpkin_core::predicates::Predicate; use pumpkin_core::propagation::ReadDomains; use pumpkin_core::state::CurrentNogood; +use pumpkin_core::state::PredicateHeap; use pumpkin_core::statistics::moving_averages::CumulativeMovingAverage; use pumpkin_core::statistics::moving_averages::MovingAverage; @@ -31,6 +33,9 @@ pub struct RecursiveMinimiser { allowed_decision_levels: HashSet, // could consider direct hashing here label_assignments: HashMap>, + /// The predicates which need to be explained in the proof. + predicates_to_log: PredicateHeap, + statistics: RecursiveMinimiserStatistics, } @@ -45,15 +50,31 @@ impl NogoodMinimiser for RecursiveMinimiser { self.initialise_minimisation_data_structures(nogood, context); + // For each predicate in the learned nogood, check whether it is implied by other + // predicates in the nogood. + for predicate in nogood.iter().copied() { + self.compute_label(predicate, context, nogood); + + // If the predicate is removable, mark it to be explained. + if context.is_proof_logging_inferences() + && self.get_predicate_label(predicate) == Label::Removable + { + self.predicates_to_log.push(predicate, context.get_state()); + } + } + + if context.is_proof_logging_inferences() { + self.add_inferences_for_removed_predicates(context); + } + // Iterate over each predicate and check whether it is a dominated predicate. let mut end_position: usize = 0; let initial_nogood_size = nogood.len(); for i in 0..initial_nogood_size { let learned_predicate = nogood[i]; - self.compute_label(learned_predicate, context, nogood); - let label = self.get_predicate_label(learned_predicate); + // Keep the predicate in case it was not deemed deemed redundant. // Note that in other cases, since 'end_position' is not incremented, // the predicate is effectively removed. @@ -123,7 +144,7 @@ impl RecursiveMinimiser { // Due to ownership rules, we have to take ownership of the reason. // TODO: Reuse the allocation if it becomes a bottleneck. let mut reason = vec![]; - context.get_propagation_reason( + let _ = context.get_propagation_reason_without_proof_log( input_predicate, CurrentNogood::from(current_nogood), &mut reason, @@ -136,10 +157,6 @@ impl RecursiveMinimiser { .unwrap() == 0 { - // The minimisation can introduce new inferences in the proof. If those inferences - // contain root-level antecedents, which we identified here, we need to make sure - // the proof is aware that that root-level assignment is used. - context.explain_root_assignment(antecedent_predicate); continue; } @@ -167,6 +184,7 @@ impl RecursiveMinimiser { } } } + // If the code reaches this part (it did not get into one of the previous 'return' // statements, so all antecedents of the literal are either KEEP or REMOVABLE), // meaning this literal is REMOVABLE. @@ -221,7 +239,8 @@ impl RecursiveMinimiser { nogood: &Vec, context: &ConflictAnalysisContext, ) { - pumpkin_assert_simple!(self.current_depth == 0); + pumpkin_assert_eq_simple!(self.current_depth, 0); + pumpkin_assert_simple!(self.predicates_to_log.is_empty()); // Mark literals from the initial learned nogood. for &predicate in nogood { @@ -247,7 +266,8 @@ impl RecursiveMinimiser { } fn clean_up_minimisation(&mut self) { - pumpkin_assert_simple!(self.current_depth == 0); + pumpkin_assert_eq_simple!(self.current_depth, 0); + pumpkin_assert_simple!(self.predicates_to_log.is_empty()); self.allowed_decision_levels.clear(); self.label_assignments.clear(); @@ -257,6 +277,34 @@ impl RecursiveMinimiser { pumpkin_assert_moderate!(self.current_depth <= 500); self.current_depth == 500 } + + /// Adds the inferences for predicates that will be removed from the learned nogood. + /// + /// Assumes `self.predicates_to_log` is initialized with the predicates that have the + /// `Removable` label. + fn add_inferences_for_removed_predicates(&mut self, context: &mut ConflictAnalysisContext) { + while let Some(predicate) = self.predicates_to_log.pop() { + if context.get_checkpoint_for_predicate(predicate).unwrap() == 0 { + context.explain_root_assignment(predicate); + continue; + } + + if [Label::Keep, Label::Poison].contains(&self.get_predicate_label(predicate)) { + continue; + } + + let mut reason_buffer = vec![]; + let _ = context.get_propagation_reason( + predicate, + CurrentNogood::empty(), + &mut reason_buffer, + ); + + for antecedent in reason_buffer { + self.predicates_to_log.push(antecedent, context.get_state()); + } + } + } } #[derive(PartialEq, Copy, Clone, Debug)] diff --git a/pumpkin-crates/conflict-resolvers/src/minimisers/semantic_minimiser.rs b/pumpkin-crates/conflict-resolvers/src/minimisers/semantic_minimiser.rs index 6f4e827ed..6bf99af68 100644 --- a/pumpkin-crates/conflict-resolvers/src/minimisers/semantic_minimiser.rs +++ b/pumpkin-crates/conflict-resolvers/src/minimisers/semantic_minimiser.rs @@ -41,11 +41,10 @@ create_statistics_struct!(SemanticMinimiserStatistics { impl Default for SemanticMinimiser { fn default() -> Self { - let mapping = |x: &DomainId| x.id() as usize; Self { original_domains: Default::default(), domains: Default::default(), - present_ids: SparseSet::new(vec![], mapping), + present_ids: SparseSet::new(vec![]), helper: Vec::default(), mode: SemanticMinimisationMode::EnableEqualityMerging, statistics: SemanticMinimiserStatistics::default(), @@ -81,6 +80,7 @@ impl NogoodMinimiser for SemanticMinimiser { return; } self.domains[domain_id].add_domain_description_to_vector( + context, *domain_id, &self.original_domains[domain_id], &mut self.helper, @@ -149,6 +149,7 @@ impl SemanticMinimiser { fn grow(&mut self, lower_bound: i32, upper_bound: i32, holes: Vec) { let mut initial_domain = SimpleIntegerDomain { + assigned_to: None, lower_bound, upper_bound, holes: HashSet::from_iter(holes.iter().cloned()), @@ -177,6 +178,14 @@ impl SemanticMinimiser { #[derive(Clone, Default, Debug)] struct SimpleIntegerDomain { + /// The value this domain was assigned to with an equality predicate. + /// + /// An input nogood `[x == 3] -> false` can be minimized to `[x >= 3] -> false` + /// if `[x <= 3]` is an initial domain bound. However, that initial domain bound + /// should be logged to the proof. This is only necessary in this situation, since if the + /// domain is assigned by a conjunction of multiple predicates, those will all have been + /// justified to the proof before reaching minimization. + assigned_to: Option, lower_bound: i32, upper_bound: i32, holes: HashSet, @@ -202,6 +211,8 @@ impl SimpleIntegerDomain { } fn assign(&mut self, value: i32) { + self.assigned_to = Some(value); + // If the domains are inconsistent, or if the assigned value would make the domain // inconsistent, declare inconsistency and stop. if self.lower_bound > self.upper_bound @@ -250,6 +261,7 @@ impl SimpleIntegerDomain { fn add_domain_description_to_vector( &self, + context: &mut ConflictAnalysisContext<'_>, domain_id: DomainId, original_domain: &SimpleIntegerDomain, description: &mut Vec, @@ -269,10 +281,20 @@ impl SimpleIntegerDomain { // Add bounds but avoid root assignments. if self.lower_bound != original_domain.lower_bound { description.push(predicate![domain_id >= self.lower_bound]); + } else if self + .assigned_to + .is_some_and(|value| value == original_domain.lower_bound) + { + context.log_domain_inference(predicate![domain_id >= original_domain.lower_bound]); } if self.upper_bound != original_domain.upper_bound { description.push(predicate![domain_id <= self.upper_bound]); + } else if self + .assigned_to + .is_some_and(|value| value == original_domain.upper_bound) + { + context.log_domain_inference(predicate![domain_id <= original_domain.upper_bound]); } // Add nonroot holes. diff --git a/pumpkin-crates/conflict-resolvers/src/resolvers/resolution_resolver.rs b/pumpkin-crates/conflict-resolvers/src/resolvers/resolution_resolver.rs index 1996fc7f8..cd2c763f8 100644 --- a/pumpkin-crates/conflict-resolvers/src/resolvers/resolution_resolver.rs +++ b/pumpkin-crates/conflict-resolvers/src/resolvers/resolution_resolver.rs @@ -191,7 +191,8 @@ impl ResolutionResolver { // 2) Get the reason for the predicate and add it to the nogood. self.reason_buffer.clear(); - context.get_propagation_reason( + + let _ = context.get_propagation_reason( next_predicate, CurrentNogood::new( &self.to_process_heap, @@ -206,7 +207,7 @@ impl ResolutionResolver { } } - self.extract_final_nogood(context) + self.extract_final_nogood(context); } /// Clears all data structures to prepare for the new conflict analysis. diff --git a/pumpkin-crates/core/Cargo.toml b/pumpkin-crates/core/Cargo.toml index dc0f5d410..2e242978b 100644 --- a/pumpkin-crates/core/Cargo.toml +++ b/pumpkin-crates/core/Cargo.toml @@ -14,16 +14,16 @@ workspace = true pumpkin-checking = { version = "0.3.0", path = "../checking" } thiserror = "2.0.12" log = "0.4.17" -bitfield = "0.14.0" +bitfield = "0.19.4" enumset = "1.1.2" fnv = "1.0.7" # We require features which are on the `main` branch of the repository but are not on crates.io -rand = { version = "0.8.5", features = [ "small_rng", "alloc" ] } +rand = { version = "0.10.1", features = [ "alloc" ] } once_cell = "1.19.0" -downcast-rs = "1.2.1" +downcast-rs = "2.0.2" drcp-format = { version = "0.3.1", path = "../../drcp-format" } -convert_case = "0.8.0" -itertools = "0.13.0" -bitfield-struct = "0.9.2" +convert_case = "0.11.0" +itertools = "0.14.0" +bitfield-struct = "0.13.0" num = "0.4.3" enum-map = "2.7.3" clap = { version = "4.5.40", optional = true, features=["derive"] } @@ -33,12 +33,17 @@ flate2 = { version = "1.1.2" } [target.'cfg(target_arch = "wasm32")'.dependencies] web-time = "1.1" -getrandom = { version = "0.2", features = ["js"] } +getrandom = { version = "0.4.2", features = ["wasm_js"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3" [features] check-propagations = [] +check-deductions = [] debug-checks = [] +hl-checks = [] clap = ["dep:clap"] + +[dev-dependencies] +test-log = "0.2.20" diff --git a/pumpkin-crates/core/clippy.toml b/pumpkin-crates/core/clippy.toml index 5ac8d8e6a..6677c5e9d 100644 --- a/pumpkin-crates/core/clippy.toml +++ b/pumpkin-crates/core/clippy.toml @@ -1,7 +1,11 @@ disallowed-types = [ { path = "std::collections::HashSet", reason = "use pumpkin_solver::core::containers::HashSet" }, { path = "std::collections::HashMap", reason = "use pumpkin_solver::core::containers::HashMap" }, - { path = "rand::RngCore", reason = "use pumpkin_solver::basic_types::Random" }, { path = "rand::Rng", reason = "use pumpkin_solver::basic_types::Random" }, + { path = "rand::RngExt", reason = "use pumpkin_solver::basic_types::Random" }, { path = "rand::SeedableRng", reason = "use pumpkin_solver::basic_types::Random" }, ] + +allowed-duplicate-crates = [ + "wit-bindgen", +] diff --git a/pumpkin-crates/core/src/api/mod.rs b/pumpkin-crates/core/src/api/mod.rs index 854be5cdf..22f3a90d7 100644 --- a/pumpkin-crates/core/src/api/mod.rs +++ b/pumpkin-crates/core/src/api/mod.rs @@ -121,6 +121,7 @@ pub mod state { pub use crate::engine::Conflict; pub use crate::engine::EmptyDomain; pub use crate::engine::EmptyDomainConflict; + pub use crate::engine::PredicateHeap; pub use crate::engine::PropagationStatusCP; pub use crate::engine::PropagatorConflict; pub use crate::engine::State; diff --git a/pumpkin-crates/core/src/api/solver.rs b/pumpkin-crates/core/src/api/solver.rs index e5f866dd3..8eecf1f53 100644 --- a/pumpkin-crates/core/src/api/solver.rs +++ b/pumpkin-crates/core/src/api/solver.rs @@ -568,6 +568,16 @@ impl Solver { pub fn default_brancher(&self) -> DefaultBrancher { DefaultBrancher::default_over_all_variables(self.satisfaction_solver.assignments()) } + + pub fn backup_brancher(&self) -> impl Brancher + 'static { + use crate::branching::value_selection::InDomainMin; + use crate::branching::variable_selection::InputOrder; + + IndependentVariableValueBrancher::new( + InputOrder::new2(self.satisfaction_solver.assignments().get_domains()), + InDomainMin, + ) + } } /// Proof logging methods diff --git a/pumpkin-crates/core/src/basic_types/random.rs b/pumpkin-crates/core/src/basic_types/random.rs index 7786fe708..1adaa758e 100644 --- a/pumpkin-crates/core/src/basic_types/random.rs +++ b/pumpkin-crates/core/src/basic_types/random.rs @@ -10,6 +10,11 @@ use rand::Rng; clippy::disallowed_types, reason = "we implement our random generator using rand" )] +use rand::RngExt; +#[allow( + clippy::disallowed_types, + reason = "we implement our random generator using rand" +)] use rand::SeedableRng; use crate::pumpkin_assert_moderate; @@ -96,19 +101,19 @@ where "It should hold that 0.0 <= {probability} <= 1.0" ); - self.gen_bool(probability) + self.random_bool(probability) } fn generate_usize_in_range(&mut self, range: Range) -> usize { - self.gen_range(range) + self.random_range(range) } fn generate_i32_in_range(&mut self, lb: i32, ub: i32) -> i32 { - self.gen_range(lb..=ub) + self.random_range(lb..=ub) } fn generate_f64(&mut self) -> f64 { - self.gen_range(0.0..1.0) + self.random_range(0.0..1.0) } fn get_weighted_choice(&mut self, weights: &[f64]) -> Option { diff --git a/pumpkin-crates/core/src/basic_types/trail.rs b/pumpkin-crates/core/src/basic_types/trail.rs index ce3bb26c8..0f9f978ed 100644 --- a/pumpkin-crates/core/src/basic_types/trail.rs +++ b/pumpkin-crates/core/src/basic_types/trail.rs @@ -73,6 +73,11 @@ impl Trail { pub(crate) fn pop(&mut self) -> Option { self.trail.pop() } + + /// Get the trail position of the last predicate assigned at the given decision level. + pub(crate) fn get_trail_position_at_decision_level(&self, decision_level: usize) -> usize { + self.trail_delimiter[decision_level] - 1 + } } impl Deref for Trail { diff --git a/pumpkin-crates/core/src/branching/brancher.rs b/pumpkin-crates/core/src/branching/brancher.rs index 938b67fd7..92e2c0243 100644 --- a/pumpkin-crates/core/src/branching/brancher.rs +++ b/pumpkin-crates/core/src/branching/brancher.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use enum_map::Enum; #[cfg(doc)] @@ -33,7 +35,7 @@ use crate::statistics::StatisticLogger; /// /// If the [`Brancher`] (or any component thereof) is implemented incorrectly then the /// behaviour of the solver is undefined. -pub trait Brancher { +pub trait Brancher: Debug { /// Logs statistics of the brancher using the provided [`StatisticLogger`]. /// /// It is recommended to create a struct through the [`create_statistics_struct!`] macro! diff --git a/pumpkin-crates/core/src/branching/branchers/alternating/alternating_brancher.rs b/pumpkin-crates/core/src/branching/branchers/alternating/alternating_brancher.rs index 36d5d5af2..0e0dd17ee 100644 --- a/pumpkin-crates/core/src/branching/branchers/alternating/alternating_brancher.rs +++ b/pumpkin-crates/core/src/branching/branchers/alternating/alternating_brancher.rs @@ -48,7 +48,7 @@ impl } } -impl Brancher +impl Brancher for AlternatingBrancher { fn next_decision(&mut self, context: &mut SelectionContext) -> Option { diff --git a/pumpkin-crates/core/src/branching/branchers/alternating/strategies/mod.rs b/pumpkin-crates/core/src/branching/branchers/alternating/strategies/mod.rs index 4f39ec1b8..f91638a50 100644 --- a/pumpkin-crates/core/src/branching/branchers/alternating/strategies/mod.rs +++ b/pumpkin-crates/core/src/branching/branchers/alternating/strategies/mod.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use crate::branching::Brancher; use crate::branching::BrancherEvent; use crate::branching::SelectionContext; @@ -7,7 +9,7 @@ use crate::results::SolutionReference; /// Defines methods for selecting which of two branching strategies to use; the default or the /// other brancher. -pub trait AlternatingStrategy { +pub trait AlternatingStrategy: Debug { /// Called when the next decision is made by the [`AlternatingBrancher`]. Returns true if the /// default brancher should be used and false otherwise. /// diff --git a/pumpkin-crates/core/src/branching/branchers/dynamic_brancher.rs b/pumpkin-crates/core/src/branching/branchers/dynamic_brancher.rs index bd8c95f3c..8b0746b20 100644 --- a/pumpkin-crates/core/src/branching/branchers/dynamic_brancher.rs +++ b/pumpkin-crates/core/src/branching/branchers/dynamic_brancher.rs @@ -29,6 +29,7 @@ use crate::statistics::StatisticLogger; /// [`DynamicBrancher::on_solution`] are called at the appropriate times as these methods ensure /// that the index to the current brancher to try is reset. If these methods are not called at the /// appropriate time then it will (likely) lead to incomplete solutions being returned! +#[derive(Debug)] pub struct DynamicBrancher { branchers: Vec>, brancher_index: usize, @@ -37,12 +38,6 @@ pub struct DynamicBrancher { relevant_events: Vec, } -impl Debug for DynamicBrancher { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("DynamicBrancher").finish() - } -} - impl DynamicBrancher { /// Creates a new [`DynamicBrancher`] with the provided `branchers`. It will attempt to use the /// `branchers` in the order in which they were provided. diff --git a/pumpkin-crates/core/src/branching/branchers/independent_variable_value_brancher.rs b/pumpkin-crates/core/src/branching/branchers/independent_variable_value_brancher.rs index 0fe83ce1e..9dec63303 100644 --- a/pumpkin-crates/core/src/branching/branchers/independent_variable_value_brancher.rs +++ b/pumpkin-crates/core/src/branching/branchers/independent_variable_value_brancher.rs @@ -1,6 +1,7 @@ //! A [`Brancher`] which simply switches uses a single [`VariableSelector`] and a single //! [`ValueSelector`]. +use std::fmt::Debug; use std::marker::PhantomData; use crate::basic_types::SolutionReference; @@ -17,6 +18,7 @@ use crate::engine::variables::DomainId; #[derive(Debug)] pub struct IndependentVariableValueBrancher where + Var: Debug, VariableSelect: VariableSelector, ValueSelect: ValueSelector, { @@ -34,6 +36,7 @@ where impl IndependentVariableValueBrancher where + Var: Debug, VariableSelect: VariableSelector, ValueSelect: ValueSelector, { @@ -49,6 +52,7 @@ where impl Brancher for IndependentVariableValueBrancher where + Var: Debug, VariableSelect: VariableSelector, ValueSelect: ValueSelector, { diff --git a/pumpkin-crates/core/src/branching/value_selection/value_selector.rs b/pumpkin-crates/core/src/branching/value_selection/value_selector.rs index 7c446a150..ee2292da6 100644 --- a/pumpkin-crates/core/src/branching/value_selection/value_selector.rs +++ b/pumpkin-crates/core/src/branching/value_selection/value_selector.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use crate::basic_types::SolutionReference; #[cfg(doc)] use crate::branching::Brancher; @@ -17,7 +19,7 @@ use crate::engine::variables::DomainId; /// A trait containing the interface for [`ValueSelector`]s, /// specifying the appropriate hooks into the solver and the methods required for selecting a value /// for a given variable. -pub trait ValueSelector { +pub trait ValueSelector: Debug { /// Determines which value in the domain of `decision_variable` to branch next on. /// The domain of the `decision_variable` variable should have at least 2 values in it (as it /// otherwise should not have been selected as `decision_variable`). Returns a diff --git a/pumpkin-crates/core/src/branching/variable_selection/input_order.rs b/pumpkin-crates/core/src/branching/variable_selection/input_order.rs index 59928a46f..c7e965b59 100644 --- a/pumpkin-crates/core/src/branching/variable_selection/input_order.rs +++ b/pumpkin-crates/core/src/branching/variable_selection/input_order.rs @@ -22,6 +22,12 @@ impl InputOrder { variables: variables.to_vec(), } } + + pub fn new2(variables: impl IntoIterator) -> Self { + InputOrder { + variables: variables.into_iter().collect(), + } + } } impl VariableSelector for InputOrder { diff --git a/pumpkin-crates/core/src/branching/variable_selection/random.rs b/pumpkin-crates/core/src/branching/variable_selection/random.rs index 46a1c1627..dbb0441f7 100644 --- a/pumpkin-crates/core/src/branching/variable_selection/random.rs +++ b/pumpkin-crates/core/src/branching/variable_selection/random.rs @@ -2,7 +2,6 @@ use super::VariableSelector; use crate::branching::BrancherEvent; use crate::branching::SelectionContext; use crate::containers::SparseSet; -use crate::containers::StorageKey; use crate::variables::DomainId; /// A [`VariableSelector`] which selects a random unfixed variable. @@ -15,9 +14,7 @@ impl RandomSelector { pub fn new(variables: impl IntoIterator) -> Self { // Note the -1 due to the fact that the indices of the domain ids start at 1 Self { - variables: SparseSet::new(variables.into_iter().collect(), |element| { - element.index() - 1 - }), + variables: SparseSet::new(variables.into_iter().collect()), } } diff --git a/pumpkin-crates/core/src/branching/variable_selection/variable_selector.rs b/pumpkin-crates/core/src/branching/variable_selection/variable_selector.rs index 16e4aef74..34fd85410 100644 --- a/pumpkin-crates/core/src/branching/variable_selection/variable_selector.rs +++ b/pumpkin-crates/core/src/branching/variable_selection/variable_selector.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + #[cfg(doc)] use crate::branching::Brancher; use crate::branching::SelectionContext; @@ -14,7 +16,7 @@ use crate::engine::variables::DomainId; /// A trait containing the interface for [`VariableSelector`]s, /// specifying the appropriate hooks into the solver and the methods required for selecting /// variables. -pub trait VariableSelector { +pub trait VariableSelector: Debug { /// Determines which variable to select next if there are any left to branch on. /// Should only return [`None`] when all variables which have been passed to the /// [`VariableSelector`] have been assigned. Otherwise it should return the variable to diff --git a/pumpkin-crates/core/src/conflict_resolving/conflict_analysis_context.rs b/pumpkin-crates/core/src/conflict_resolving/conflict_analysis_context.rs index 0a5173925..7d0f947b3 100644 --- a/pumpkin-crates/core/src/conflict_resolving/conflict_analysis_context.rs +++ b/pumpkin-crates/core/src/conflict_resolving/conflict_analysis_context.rs @@ -1,5 +1,8 @@ use std::fmt::Debug; +use itertools::Itertools; +use log::debug; + use crate::Random; use crate::basic_types::StoredConflictInfo; use crate::branching::Brancher; @@ -135,16 +138,35 @@ impl ConflictAnalysisContext<'_> { .collect() } + /// Get the reason for `predicate` being true, without logging that reason to the proof. + pub fn get_propagation_reason_without_proof_log( + &mut self, + predicate: Predicate, + current_nogood: CurrentNogood<'_>, + reason_buffer: &mut (impl Extend + AsRef<[Predicate]>), + ) -> Option { + Self::get_propagation_reason_inner( + predicate, + current_nogood, + &mut ProofLog::default(), + self.unit_nogood_inference_codes, + reason_buffer, + self.state, + ) + } + /// Compute the reason for `predicate` being true. The reason will be stored in /// `reason_buffer`. /// /// If `predicate` is not true, or it is a decision, then this function will panic. + /// + /// Returns the [`InferenceCode`] if one was attached to this reason. pub fn get_propagation_reason( &mut self, predicate: Predicate, current_nogood: CurrentNogood<'_>, reason_buffer: &mut (impl Extend + AsRef<[Predicate]>), - ) { + ) -> Option { Self::get_propagation_reason_inner( predicate, current_nogood, @@ -152,13 +174,17 @@ impl ConflictAnalysisContext<'_> { self.unit_nogood_inference_codes, reason_buffer, self.state, - ); + ) } /// Returns the last decision which was made by the solver (if such a decision exists). pub fn find_last_decision(&mut self) -> Option { self.state.assignments.find_last_decision() } + + pub fn is_proof_logging_inferences(&self) -> bool { + self.proof_log.is_logging_inferences() + } } /// Methods used for proof logging @@ -175,13 +201,47 @@ impl ConflictAnalysisContext<'_> { ); } + /// Log an inference to the proof. + pub fn log_inference( + &mut self, + inference_code: InferenceCode, + premises: impl IntoIterator + Clone, + consequent: Option, + ) { + let _ = self + .proof_log + .log_inference( + &mut self.state.constraint_tags, + inference_code, + premises, + consequent, + &self.state.variable_names, + &self.state.assignments, + ) + .expect("Failed to write proof log"); + } + + /// Indicate to the proof that the initial domain `predicate` is used in the next + /// deduction. + pub fn log_domain_inference(&mut self, predicate: Predicate) { + let _ = self + .proof_log + .log_domain_inference( + predicate, + &self.state.variable_names, + &mut self.state.constraint_tags, + &self.state.assignments, + ) + .expect("Failed to write proof log"); + } + /// Log a deduction (learned nogood) to the proof. /// /// The inferences and marked propagations are assumed to be recorded in reverse-application /// order. pub fn log_deduction( &mut self, - premises: impl IntoIterator, + premises: impl IntoIterator + Clone, ) -> ConstraintTag { self.proof_log .log_deduction( @@ -210,7 +270,7 @@ impl ConflictAnalysisContext<'_> { /// which the solver backtracked. pub fn process_learned_nogood( &mut self, - learned_nogood_predicates: Vec, + mut learned_nogood_predicates: Vec, lbd: u32, ) -> usize { // important to notify about the conflict _before_ backtracking removes literals from @@ -219,6 +279,11 @@ impl ConflictAnalysisContext<'_> { self.restart_strategy .notify_conflict(lbd, self.state.assignments.get_pruned_value_count()); + learned_nogood_predicates.sort(); + debug!( + "Learned {} -> <= -1", + learned_nogood_predicates.iter().format(" & "), + ); let learned_nogood = LearnedNogood::create_from_vec(learned_nogood_predicates, self); let constraint_tag = self.log_deduction(learned_nogood.predicates.iter().copied()); @@ -264,6 +329,8 @@ impl ConflictAnalysisContext<'_> { /// `reason_buffer`. /// /// If `predicate` is not true, or it is a decision, then this function will panic. + /// + /// Returns the [`InferenceCode`] if one was attached to this reason. pub(crate) fn get_propagation_reason_inner( predicate: Predicate, current_nogood: CurrentNogood<'_>, @@ -271,16 +338,16 @@ impl ConflictAnalysisContext<'_> { unit_nogood_inference_codes: &HashMap, reason_buffer: &mut (impl Extend + AsRef<[Predicate]>), state: &mut State, - ) { + ) -> Option { let inference_code = state.get_propagation_reason(predicate, reason_buffer, current_nogood); - if inference_code.is_some() { + if let Some(ref ic) = inference_code { let trail_index = state.trail_position(predicate).expect( "an inference code is only present if the propagated predicate is on the trail", ); let trail_entry = state.assignments.get_trail_entry(trail_index); - let Some((reason_ref, inference_code)) = trail_entry.reason else { - return; + let Some(reason_ref) = trail_entry.reason else { + return inference_code; }; let propagator_id = state.reason_store.get_propagator(reason_ref); @@ -296,7 +363,7 @@ impl ConflictAnalysisContext<'_> { // // It could be that the predicate is implied by another unit nogood - let inference_code = unit_nogood_inference_codes + let unit_ic = unit_nogood_inference_codes .get(&predicate) .or_else(|| { // It could be the case that we attempt to get the reason for the predicate @@ -311,7 +378,7 @@ impl ConflictAnalysisContext<'_> { let _ = proof_log.log_inference( &mut state.constraint_tags, - inference_code.clone(), + unit_ic.clone(), [], Some(predicate), &state.variable_names, @@ -321,7 +388,7 @@ impl ConflictAnalysisContext<'_> { // Otherwise we log the inference which was used to derive the nogood let _ = proof_log.log_inference( &mut state.constraint_tags, - inference_code, + ic.clone(), reason_buffer.as_ref().iter().copied(), Some(predicate), &state.variable_names, @@ -329,6 +396,8 @@ impl ConflictAnalysisContext<'_> { ); } } + + inference_code } fn compute_conflict_nogood( @@ -340,7 +409,7 @@ impl ConflictAnalysisContext<'_> { // Look up the reason for the bound that changed. // The reason for changing the bound cannot be a decision, so we can safely unwrap. let mut empty_domain_reason: Vec = vec![]; - let _ = self.state.reason_store.get_or_compute( + let trigger_inference_code = self.state.reason_store.get_or_compute( conflict.trigger_reason.expect("in conflict analysis the empty domain conflict is always triggered by a propagation"), ExplanationContext::without_working_nogood( &self.state.assignments, @@ -351,12 +420,13 @@ impl ConflictAnalysisContext<'_> { ), &mut self.state.propagators, &mut empty_domain_reason, + conflict.trigger_predicate, ); // We also need to log this last propagation to the proof log as an inference. let _ = self.proof_log.log_inference( &mut self.state.constraint_tags, - conflict.trigger_inference_code.expect("in conflict analysis the empty domain conflict is always triggered by a propagation"), + trigger_inference_code, empty_domain_reason.iter().copied(), Some(conflict.trigger_predicate), &self.state.variable_names, diff --git a/pumpkin-crates/core/src/containers/sparse_set.rs b/pumpkin-crates/core/src/containers/sparse_set.rs index b75b7135c..accc9b6ce 100644 --- a/pumpkin-crates/core/src/containers/sparse_set.rs +++ b/pumpkin-crates/core/src/containers/sparse_set.rs @@ -18,13 +18,18 @@ //! Our implementation follows [\[1\]](https://hal.science/hal-01339250/document). The [`SparseSet`] //! structure keeps track of a number of variables; the main practical consideration is that a //! function `mapping` should be provided which maps every -//! value in the domain to an index in \[0..|domain|\) in a bijective manner. +//! value in the domain to an index such that no two elements map to the same index. +//! +//! For performance, it is recommended to provide a mapping which maps an element to the +//! range `[0, |domain|]`. //! //! # Bibliography //! \[1\] V. le C. de Saint-Marcq, P. Schaus, C. Solnon, and C. Lecoutre, ‘Sparse-sets for domain //! implementation’, in CP workshop on Techniques foR Implementing Constraint programming Systems //! (TRICS), 2013, pp. 1–10. +use crate::containers::HashSet; +use crate::containers::StorageKey; use crate::pumpkin_assert_moderate; use crate::pumpkin_assert_simple; @@ -35,7 +40,7 @@ use crate::pumpkin_assert_simple; /// are the values which are currently in the domain). /// /// Note that it is required that each element contained in the domain can be -/// uniquely mapped to an index in the range [0, |D|) (i.e. the mapping function is bijective) +/// uniquely mapped to an index via the provided mapping. /// /// # Bibliography /// \[1\] V. le C. de Saint-Marcq, P. Schaus, C. Solnon, and C. Lecoutre, ‘Sparse-sets for domain @@ -54,23 +59,75 @@ pub struct SparseSet { /// A bijective function which takes as input an element `T` and returns an index in the range /// [0, |D_{original}|) to be used for retrieving values from /// [`indices`][`SparseSet::indices`] - mapping: fn(&T) -> usize, + mapping: fn(&T) -> i32, + index_offset: i32, +} + +impl SparseSet { + /// Creates a new [`SparseSet`], using [`StorageKey::index`] as the index for the elements. + /// + /// If [`StorageKey::index`] returns the same index for two elements, then this method will + /// panic. + pub fn new(input: Vec) -> Self { + Self::new_with_mapping(input, |element: &T| element.index() as i32) + } } impl SparseSet { - /// Assumption: It is assumed that `mapping` is a bijective function which - /// will return an index which is in the range [0, |D_{original}|) (where D_{original} is - /// the initial domain before any operations have been performed). - pub fn new(input: Vec, mapping: fn(&T) -> usize) -> Self { + /// Creates a new [`SparseSet`], using the provided `mapping` to map the elements to indices. + /// + /// If the provided `mapping` maps two elements in `input` to the same element then this method + /// will panic. + /// + /// For performance, it is recommended to provide a mapping which maps all elements to the + /// range `[0, |domain|]`. + pub fn new_with_mapping(input: Vec, mapping: fn(&T) -> i32) -> Self { let input_len = input.len(); + + let mut min_index = 0; + let mut max_index = 0; + + let mut used_indices = HashSet::new(); + + for element in input.iter() { + let index = (mapping)(element); + let not_previously_inserted = used_indices.insert(index); + + pumpkin_assert_simple!( + not_previously_inserted, + "Two elements in the provided `input` map to the same index." + ); + + min_index = min_index.min(index); + max_index = max_index.max(index); + } + + pumpkin_assert_simple!(min_index <= max_index); + + // Now we need to adjust the indices; we first assign everything to usize::Max + let mut indices = + std::iter::repeat_n(usize::MAX, (max_index.abs() + min_index.abs()) as usize + 1) + .collect::>(); + // Then we go over all of the elements in the domain and assign them to their appropriate + // indices + for (i, element) in input.iter().enumerate().collect::>() { + indices[((mapping)(element) - min_index) as usize] = i; + } + SparseSet { size: input_len, domain: input, - indices: (0..input_len).collect::>(), + indices, mapping, + index_offset: -min_index, } } + fn get_mapping(&self, element: &T) -> usize { + let output_index = (self.mapping)(element) + self.index_offset; + output_index.try_into().unwrap() + } + pub fn set_to_empty(&mut self) { self.indices = vec![usize::MAX; self.indices.len()]; self.domain.clear(); @@ -102,68 +159,94 @@ impl SparseSet { /// corresponding indices in [`indices`][SparseSet::indices] fn swap(&mut self, i: usize, j: usize) { self.domain.swap(i, j); - self.indices[(self.mapping)(&self.domain[i])] = i; - self.indices[(self.mapping)(&self.domain[j])] = j; + + let index_i = self.get_mapping(&self.domain[i]); + self.indices[index_i] = i; + + let index_j = self.get_mapping(&self.domain[j]); + self.indices[index_j] = j; } /// Remove the value of `to_remove` from the domain; if the value is not in the domain then this /// method does not perform any operations. pub fn remove(&mut self, to_remove: &T) { - if self.indices[(self.mapping)(to_remove)] < self.size { + if self.indices[self.get_mapping(to_remove)] < self.size { // The element is part of the domain and should be removed self.size -= 1; if self.size > 0 { - self.swap(self.indices[(self.mapping)(to_remove)], self.size); + self.swap(self.indices[self.get_mapping(to_remove)], self.size); } self.swap( - self.indices[(self.mapping)(to_remove)], + self.indices[self.get_mapping(to_remove)], self.domain.len() - 1, ); let element = self.domain.pop().expect("Has to have something to pop."); pumpkin_assert_moderate!((self.mapping)(&element) == (self.mapping)(to_remove)); - self.indices[(self.mapping)(to_remove)] = usize::MAX; - } else if self.indices[(self.mapping)(to_remove)] < self.domain.len() { + + let to_remove_index = self.get_mapping(to_remove); + self.indices[to_remove_index] = usize::MAX; + } else if self.indices[self.get_mapping(to_remove)] < self.domain.len() { self.swap( - self.indices[(self.mapping)(to_remove)], + self.indices[self.get_mapping(to_remove)], self.domain.len() - 1, ); let element = self.domain.pop().expect("Has to have something to pop."); pumpkin_assert_moderate!((self.mapping)(&element) == (self.mapping)(to_remove)); - self.indices[(self.mapping)(to_remove)] = usize::MAX; + + let to_remove_index = self.get_mapping(to_remove); + self.indices[to_remove_index] = usize::MAX; } } pub fn remove_temporarily(&mut self, to_remove: &T) { - if self.indices[(self.mapping)(to_remove)] < self.size { + if self.indices[self.get_mapping(to_remove)] < self.size { // The element is part of the domain and should be removed self.size -= 1; - self.swap(self.indices[(self.mapping)(to_remove)], self.size); + self.swap(self.indices[self.get_mapping(to_remove)], self.size); } } /// Determines whehter the `element` is contained in the domain of the sparse-set. pub fn contains(&self, element: &T) -> bool { - (self.mapping)(element) < self.indices.len() - && self.indices[(self.mapping)(element)] < self.size + self.get_mapping(element) < self.indices.len() + && self.indices[self.get_mapping(element)] < self.size } /// Accomodates the `element`. pub fn accommodate(&mut self, element: &T) { - let index = (self.mapping)(element); + let index = self.get_mapping(element); if self.indices.len() <= index { self.indices.resize(index + 1, usize::MAX); } } /// Inserts the element if it is not already contained in the sparse set. + /// + /// Note that this method does *not* check whether the insertion of this element causes clashes + /// in the provided mapping (i.e., it does not check whether two elements are now mapped to the + /// same index). pub fn insert(&mut self, element: T) { if !self.contains(&element) { self.accommodate(&element); - self.indices[(self.mapping)(&element)] = self.domain.len(); - self.domain.push(element); - self.swap(self.size, self.domain.len() - 1); + let mut index = self.indices[self.get_mapping(&element)]; + + // The index is outside of the domain, we need to readjust it + // + // If the index is `<= self.domain.len()`, then it could be that it is a temporarily + // removed variable; in this case, we do not want to add a new element, but we just want + // to place it inside of the domain again + if index >= self.domain.len() { + index = self.domain.len(); + + let element_index = self.get_mapping(&element); + self.indices[element_index] = index; + + self.domain.push(element); + } + + self.swap(self.size, index); self.size += 1; } } @@ -192,19 +275,19 @@ impl IntoIterator for SparseSet { mod tests { use super::SparseSet; - fn mapping_function(input: &u32) -> usize { - *input as usize + fn mapping_function(input: &i32) -> i32 { + *input } #[test] fn test_len() { - let sparse_set = SparseSet::new(vec![0, 1, 2], mapping_function); + let sparse_set = SparseSet::new_with_mapping(vec![0, 1, 2], mapping_function); assert_eq!(sparse_set.len(), 3); } #[test] fn removal() { - let mut sparse_set = SparseSet::new(vec![0, 1, 2], mapping_function); + let mut sparse_set = SparseSet::new_with_mapping(vec![0, 1, 2], mapping_function); sparse_set.remove(&1); assert_eq!(sparse_set.domain, vec![0, 2]); assert_eq!(sparse_set.size, 2); @@ -213,7 +296,7 @@ mod tests { #[test] fn removal_adjusts_size() { - let mut sparse_set = SparseSet::new(vec![0, 1, 2], mapping_function); + let mut sparse_set = SparseSet::new_with_mapping(vec![0, 1, 2], mapping_function); assert_eq!(sparse_set.size, 3); sparse_set.remove(&0); assert_eq!(sparse_set.size, 2); @@ -221,7 +304,7 @@ mod tests { #[test] fn remove_all_elements_leads_to_empty_set() { - let mut sparse_set = SparseSet::new(vec![0, 1, 2], mapping_function); + let mut sparse_set = SparseSet::new_with_mapping(vec![0, 1, 2], mapping_function); sparse_set.remove(&0); sparse_set.remove(&1); sparse_set.remove(&2); @@ -230,8 +313,8 @@ mod tests { #[test] fn iter1() { - let sparse_set = SparseSet::new(vec![5, 10, 2], mapping_function); - let v: Vec = sparse_set.iter().copied().collect(); + let sparse_set = SparseSet::new_with_mapping(vec![5, 10, 2], mapping_function); + let v: Vec = sparse_set.iter().copied().collect(); assert_eq!(v.len(), 3); assert!(v.contains(&10)); assert!(v.contains(&5)); @@ -240,19 +323,109 @@ mod tests { #[test] fn iter2() { - let mut sparse_set = SparseSet::new(vec![5, 10, 2], mapping_function); - sparse_set.insert(100); - sparse_set.insert(2); - sparse_set.insert(20); - sparse_set.remove(&10); - sparse_set.insert(10); - sparse_set.remove(&10); - - let v: Vec = sparse_set.iter().copied().collect(); - assert_eq!(v.len(), 5); + let mut sparse_set = SparseSet::new_with_mapping(vec![5, 10, 2], mapping_function); // 5, 10, 2 + sparse_set.insert(100); // 5, 10, 2, 100 + sparse_set.insert(2); // 5, 10, 2, 100 + sparse_set.insert(20); // 5, 10, 2, 100, 20 + sparse_set.remove(&10); // 5, 2, 100, 20 + sparse_set.insert(10); // 5, 10, 2, 100, 20 + sparse_set.remove(&10); // 5, 2, 100, 20 + + let v: Vec = sparse_set.iter().copied().collect(); + assert_eq!(v.len(), 4); assert!(v.contains(&5)); assert!(v.contains(&2)); assert!(v.contains(&100)); assert!(v.contains(&20)); + assert!(!v.contains(&10)); + } + + #[test] + fn remove_temporarily_simple() { + let mut sparse_set = SparseSet::new_with_mapping(vec![0], mapping_function); + + sparse_set.remove_temporarily(&0); + sparse_set.insert(0); + sparse_set.remove_temporarily(&0); + + assert!(sparse_set.is_empty()) + } + + #[test] + fn remove_temporarily() { + let mut sparse_set = SparseSet::new_with_mapping(vec![2, 0, 1], mapping_function); + + assert!(!sparse_set.is_empty()); + + sparse_set.remove_temporarily(&0); + sparse_set.insert(0); + sparse_set.remove_temporarily(&0); + assert!(!sparse_set.contains(&0)); + + assert!(!sparse_set.is_empty()); + + sparse_set.remove_temporarily(&0); + assert!(!sparse_set.contains(&0)); + + sparse_set.remove_temporarily(&0); + sparse_set.remove_temporarily(&2); + assert!(!sparse_set.contains(&2)); + sparse_set.remove_temporarily(&1); + assert!(!sparse_set.contains(&1)); + + assert!(sparse_set.is_empty()); + + sparse_set.insert(1); + + assert!(sparse_set.contains(&1)); + + assert!(!sparse_set.contains(&0)); + assert!(sparse_set.contains(&1)); + assert!(!sparse_set.contains(&2)); + + sparse_set.restore_temporarily_removed(); + + assert!(sparse_set.contains(&0)); + assert!(sparse_set.contains(&1)); + assert!(sparse_set.contains(&2)); + } + + #[test] + fn remove_temporarily_non_continuous() { + let mut sparse_set = SparseSet::new_with_mapping(vec![5, 10, 2], mapping_function); + sparse_set.remove_temporarily(&10); + assert!(!sparse_set.contains(&10)); + + sparse_set.remove_temporarily(&5); + sparse_set.remove_temporarily(&2); + assert!(sparse_set.is_empty()); + } + + #[test] + fn remove_temporarily_non_continuous_spanning() { + let mut sparse_set = SparseSet::new_with_mapping(vec![5, 10, -2], mapping_function); + sparse_set.remove_temporarily(&10); + assert!(!sparse_set.contains(&10)); + + sparse_set.remove_temporarily(&-2); + assert!(!sparse_set.contains(&-2)); + + sparse_set.remove_temporarily(&5); + + assert!(sparse_set.is_empty()); + } + + #[test] + fn remove_temporarily_non_continuous_negative() { + let mut sparse_set = SparseSet::new_with_mapping(vec![-5, -10, -2], mapping_function); + sparse_set.remove_temporarily(&-10); + assert!(!sparse_set.contains(&-10)); + + sparse_set.remove_temporarily(&-2); + assert!(!sparse_set.contains(&-2)); + + sparse_set.remove_temporarily(&-5); + + assert!(sparse_set.is_empty()); } } diff --git a/pumpkin-crates/core/src/engine/conflict.rs b/pumpkin-crates/core/src/engine/conflict.rs index 8687c8ebc..8fd6bcb46 100644 --- a/pumpkin-crates/core/src/engine/conflict.rs +++ b/pumpkin-crates/core/src/engine/conflict.rs @@ -49,15 +49,10 @@ impl From for Conflict { } /// A conflict because a domain became empty. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct EmptyDomainConflict { /// The predicate that caused a domain to become empty. pub trigger_predicate: Predicate, - /// The [`InferenceCode`] that accompanies triggered the conflict. - /// - /// If the empty domain is not triggered by a propagation, this is [`None`]. - pub trigger_inference_code: Option, - /// The reason for [`EmptyDomainConflict::trigger_predicate`] to be true. /// /// If the empty domain is not triggered by a propagation, this is [`None`]. @@ -77,9 +72,9 @@ impl EmptyDomainConflict { state: &mut State, reason_buffer: &mut (impl Extend + AsRef<[Predicate]>), current_nogood: CurrentNogood, - ) { - if let Some(reason_ref) = self.trigger_reason { - let _ = state.reason_store.get_or_compute( + ) -> Option { + self.trigger_reason.map(|reason_ref| { + state.reason_store.get_or_compute( reason_ref, ExplanationContext::new( &state.assignments, @@ -89,8 +84,9 @@ impl EmptyDomainConflict { ), &mut state.propagators, reason_buffer, - ); - } + self.trigger_predicate, + ) + }) } } diff --git a/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs b/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs index 08b3d031c..5cb6ceb49 100644 --- a/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs +++ b/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs @@ -5,6 +5,7 @@ use std::collections::VecDeque; use std::fmt::Debug; use std::sync::Arc; +use log::trace; #[allow( clippy::disallowed_types, reason = "any rand generator is a valid implementation of Random" @@ -145,6 +146,7 @@ pub enum ConflictResolverType { NoLearning, #[default] UIP, + HypercubeLinear, } /// Options for the [`Solver`] which determine how it behaves. @@ -162,6 +164,8 @@ pub struct SatisfactionSolverOptions { pub learning_options: LearningOptions, /// The number of MBs which are preallocated by the nogood propagator. pub memory_preallocated: usize, + /// The type of conflict resolver being used. + pub resolver_type: ConflictResolverType, } impl Default for SatisfactionSolverOptions { @@ -173,6 +177,7 @@ impl Default for SatisfactionSolverOptions { proof_log: ProofLog::default(), learning_options: LearningOptions::default(), memory_preallocated: 50, + resolver_type: Default::default(), } } } @@ -204,6 +209,11 @@ impl ConstraintSatisfactionSolver { } fn complete_proof(&mut self) { + if self.internal_parameters.resolver_type != ConflictResolverType::UIP { + return; + } + + #[derive(Debug)] struct DummyBrancher; impl Brancher for DummyBrancher { @@ -437,7 +447,7 @@ impl ConstraintSatisfactionSolver { continue; } - ConflictAnalysisContext::get_propagation_reason_inner( + let _ = ConflictAnalysisContext::get_propagation_reason_inner( predicate, CurrentNogood::empty(), context.proof_log, @@ -551,6 +561,7 @@ impl ConstraintSatisfactionSolver { Ok(()) => {} } } else { + trace!("Conflict detected @ {}", self.state.get_checkpoint()); if self.get_checkpoint() == 0 { self.complete_proof(); self.solver_state.declare_infeasible(); @@ -611,6 +622,12 @@ impl ConstraintSatisfactionSolver { self.new_checkpoint(); + trace!( + "Branching {} @ {}", + decision_predicate, + self.state.get_checkpoint() + ); + // Note: This also checks that the decision predicate is not already true. That is a // stronger check than the `.expect(...)` used later on when handling the result of // `Assignments::post_predicate`. @@ -755,9 +772,13 @@ impl ConstraintSatisfactionSolver { for trail_idx in start_trail_index..self.state.trail_len() { let entry = self.state.trail_entry(trail_idx); - let (_, inference_code) = entry - .reason - .expect("Added by a propagator and must therefore have a reason"); + + // Get the conjunction of predicates explaining the propagation along with the + // InferenceCode identifying the explanation algorithm. + let mut reason = vec![]; + let inference_code = self + .state + .get_propagation_reason_trail_entry(trail_idx, &mut reason); if !self.internal_parameters.proof_log.is_logging_inferences() { // In case we are not logging inferences, we only need to keep track @@ -769,11 +790,6 @@ impl ConstraintSatisfactionSolver { continue; } - // Get the conjunction of predicates explaining the propagation. - let mut reason = vec![]; - self.state - .get_propagation_reason_trail_entry(trail_idx, &mut reason); - let propagated = entry.predicate; // The proof inference for the propagation `R -> l` is `R /\ ~l -> false`. @@ -983,7 +999,23 @@ impl ConstraintSatisfactionSolver { return Err(ConstraintOperationError::InfeasibleClause); } + let inference_code = InferenceCode::new(constraint_tag, NogoodLabel); if are_all_falsified_at_root { + // Since the propagation is not actually performed, we log the inference + // explicitly here for the proof. + let _ = self + .internal_parameters + .proof_log + .log_inference( + &mut self.state.constraint_tags, + inference_code, + predicates.iter().copied(), + None, + &self.state.variable_names, + &self.state.assignments, + ) + .expect("failed to write to proof"); + finalize_proof(FinalizingContext { conflict: predicates.into(), proof_log: &mut self.internal_parameters.proof_log, @@ -997,7 +1029,6 @@ impl ConstraintSatisfactionSolver { return Err(ConstraintOperationError::InfeasibleClause); } - let inference_code = InferenceCode::new(constraint_tag, NogoodLabel); if let Err(constraint_operation_error) = self.add_nogood(predicates, inference_code) { let _ = self.conclude_proof_unsat(); diff --git a/pumpkin-crates/core/src/engine/cp/assignments.rs b/pumpkin-crates/core/src/engine/cp/assignments.rs index d0dad662e..31d786dfe 100644 --- a/pumpkin-crates/core/src/engine/cp/assignments.rs +++ b/pumpkin-crates/core/src/engine/cp/assignments.rs @@ -8,7 +8,6 @@ use crate::engine::predicates::predicate::PredicateType; use crate::engine::variables::DomainGeneratorIterator; use crate::engine::variables::DomainId; use crate::predicate; -use crate::proof::InferenceCode; use crate::pumpkin_assert_eq_moderate; use crate::pumpkin_assert_eq_simple; use crate::pumpkin_assert_moderate; @@ -108,8 +107,6 @@ impl Assignments { } pub(crate) fn get_trail_entry(&self, index: usize) -> ConstraintProgrammingTrailEntry { - // The clone is required because of InferenceCode is not copy. However, it is a - // reference-counted type, so cloning is cheap. self.trail[index].clone() } @@ -397,15 +394,13 @@ impl Assignments { } } -pub(crate) type AssignmentReason = (ReasonRef, InferenceCode); - // methods to change the domains impl Assignments { fn tighten_lower_bound( &mut self, domain_id: DomainId, new_lower_bound: i32, - reason: Option, + reason: Option, ) -> Result { // No need to do any changes if the new lower bound is weaker. if new_lower_bound <= self.get_lower_bound(domain_id) { @@ -445,7 +440,7 @@ impl Assignments { &mut self, domain_id: DomainId, new_upper_bound: i32, - reason: Option, + reason: Option, ) -> Result { // No need to do any changes if the new upper bound is weaker. if new_upper_bound >= self.get_upper_bound(domain_id) { @@ -485,7 +480,7 @@ impl Assignments { &mut self, domain_id: DomainId, assigned_value: i32, - reason: Option, + reason: Option, ) -> Result { let mut update_took_place = false; @@ -532,7 +527,7 @@ impl Assignments { &mut self, domain_id: DomainId, removed_value_from_domain: i32, - reason: Option, + reason: Option, ) -> Result { // No need to do any changes if the value is not present anyway. if !self.domains[domain_id].contains(removed_value_from_domain) { @@ -583,7 +578,7 @@ impl Assignments { pub(crate) fn post_predicate( &mut self, predicate: Predicate, - reason: Option, + reason: Option, notification_engine: &mut NotificationEngine, ) -> Result { let (lower_bound_before, upper_bound_before) = self.bounds[predicate.get_domain()]; @@ -750,15 +745,15 @@ impl Assignments { } /// todo: This is a temporary hack, not to be used in general. - pub(crate) fn remove_last_trail_element(&mut self) -> (Predicate, ReasonRef, InferenceCode) { + pub(crate) fn remove_last_trail_element(&mut self) -> (Predicate, ReasonRef) { let entry = self.trail.pop().unwrap(); let domain_id = entry.predicate.get_domain(); self.domains[domain_id].undo_trail_entry(&entry); self.update_bounds_snapshot(domain_id); - let (reason, inference_code) = entry.reason.unwrap(); + let reason_ref = entry.reason.unwrap(); - (entry.predicate, reason, inference_code) + (entry.predicate, reason_ref) } /// Get the number of values pruned from all the domains. @@ -781,13 +776,74 @@ impl Assignments { .iter() .find_map(|entry| { if entry.predicate == predicate { - entry.reason.as_ref().map(|(reason_ref, _)| *reason_ref) + entry.reason } else { None } }) .unwrap_or_else(|| panic!("could not find a reason for predicate {predicate}")) } + + /// Get the last trail position that is on the given checkpoint. + pub(crate) fn get_trail_position_at_checkpoint(&self, checkpoint: usize) -> usize { + self.trail.get_trail_position_at_decision_level(checkpoint) + } + + /// Same as [ Self::evaluate_predicate`] but at a given trail position. + pub(crate) fn evaluate_predicate_at_trail_position( + &self, + predicate: Predicate, + trail_position: usize, + ) -> Option { + let domain_id = predicate.get_domain(); + let value = predicate.get_right_hand_side(); + + let lb = self.get_lower_bound_at_trail_position(domain_id, trail_position); + let ub = self.get_upper_bound_at_trail_position(domain_id, trail_position); + + match predicate.get_predicate_type() { + PredicateType::LowerBound => { + if lb >= value { + Some(true) + } else if ub < value { + Some(false) + } else { + None + } + } + PredicateType::UpperBound => { + if ub <= value { + Some(true) + } else if lb > value { + Some(false) + } else { + None + } + } + PredicateType::NotEqual => { + if !self.is_value_in_domain_at_trail_position(domain_id, value, trail_position) { + Some(true) + } else if lb == ub { + // Previous branch concluded the value is not in the domain, so if the variable + // is assigned, then it is assigned to the not equals value. + pumpkin_assert_simple!(lb == value); + Some(false) + } else { + None + } + } + PredicateType::Equal => { + if !self.is_value_in_domain_at_trail_position(domain_id, value, trail_position) { + Some(false) + } else if lb == ub { + pumpkin_assert_moderate!(lb == value); + Some(true) + } else { + None + } + } + } + } } #[derive(Clone, Debug)] @@ -800,7 +856,7 @@ pub(crate) struct ConstraintProgrammingTrailEntry { /// Stores the a reference to the reason in the `ReasonStore`, only makes sense if a /// propagation took place, e.g., does _not_ make sense in the case of a decision or if /// the update was due to synchronisation from the propositional trail. - pub(crate) reason: Option, + pub(crate) reason: Option, } #[derive(Clone, Copy, Debug)] diff --git a/pumpkin-crates/core/src/engine/cp/mod.rs b/pumpkin-crates/core/src/engine/cp/mod.rs index 2be12ab01..c0af305bb 100644 --- a/pumpkin-crates/core/src/engine/cp/mod.rs +++ b/pumpkin-crates/core/src/engine/cp/mod.rs @@ -46,8 +46,10 @@ mod tests { let result = context.post( predicate![domain >= 2], - conjunction!(), - &InferenceCode::unknown_label(ConstraintTag::create_from_index(0)), + ( + conjunction!(), + &InferenceCode::unknown_label(ConstraintTag::create_from_index(0)), + ), ); assert!(result.is_ok()); } @@ -75,8 +77,10 @@ mod tests { let result = context.post( predicate![domain <= 15], - conjunction!(), - &InferenceCode::unknown_label(ConstraintTag::create_from_index(0)), + ( + conjunction!(), + &InferenceCode::unknown_label(ConstraintTag::create_from_index(0)), + ), ); assert!(result.is_ok()); } @@ -104,8 +108,10 @@ mod tests { let result = context.post( predicate![domain != 15], - conjunction!(), - &InferenceCode::unknown_label(ConstraintTag::create_from_index(0)), + ( + conjunction!(), + &InferenceCode::unknown_label(ConstraintTag::create_from_index(0)), + ), ); assert!(result.is_ok()); } diff --git a/pumpkin-crates/core/src/engine/cp/reason.rs b/pumpkin-crates/core/src/engine/cp/reason.rs index fb1e8525c..09a5c96ca 100644 --- a/pumpkin-crates/core/src/engine/cp/reason.rs +++ b/pumpkin-crates/core/src/engine/cp/reason.rs @@ -4,9 +4,13 @@ use crate::basic_types::PropositionalConjunction; use crate::basic_types::Trail; #[cfg(doc)] use crate::containers::KeyedVec; +use crate::predicate; use crate::predicates::Predicate; +use crate::proof::InferenceCode; use crate::propagation::ExplanationContext; +use crate::propagation::Propagator; use crate::propagation::PropagatorId; +use crate::propagation::ReadDomains; use crate::propagation::store::PropagatorStore; use crate::pumpkin_assert_simple; @@ -33,31 +37,38 @@ impl ReasonStore { Slot { store: self } } - /// Evaluate the reason with the given reference, and write the predicates to - /// `destination_buffer`. + /// Evaluate the reason with the given reference, write the predicates to `destination_buffer`, + /// and return the [`InferenceCode`] associated with the reason. + /// + /// # Panics + /// Panics if `reference` does not exist in the store. pub(crate) fn get_or_compute( &self, reference: ReasonRef, context: ExplanationContext<'_>, propagators: &mut PropagatorStore, destination_buffer: &mut impl Extend, - ) -> bool { - let Some(reason) = self.trail.get(reference.0 as usize) else { - return false; - }; - - reason - .1 - .compute(context, reason.0, propagators, destination_buffer); - - true + predicate_to_explain: Predicate, + ) -> InferenceCode { + let reason = self + .trail + .get(reference.0 as usize) + .expect("cannot get reason for predicate"); + + reason.1.compute( + context, + reason.0, + propagators, + destination_buffer, + predicate_to_explain, + ) } - pub(crate) fn get_lazy_code(&self, reference: ReasonRef) -> Option<&u64> { + pub(crate) fn get_lazy_code(&self, reference: ReasonRef) -> Option { match self.trail.get(reference.0 as usize) { Some(reason) => match &reason.1 { - StoredReason::Eager(_) => None, - StoredReason::DynamicLazy(code) => Some(code), + StoredReason::Eager(_, _) => None, + StoredReason::DynamicLazy(code) => Some(*code), }, None => None, } @@ -90,14 +101,15 @@ pub(crate) struct ReasonRef(pub(crate) u32); #[derive(Debug)] pub enum Reason { /// An eager reason contains the propositional conjunction with the reason, without the - /// propagated predicate. - Eager(PropositionalConjunction), + /// propagated predicate, and the [`InferenceCode`] identifying the explanation algorithm. + Eager(PropositionalConjunction, InferenceCode), /// A lazy reason, which is computed on-demand rather than up-front. This is also referred to /// as a 'backward' reason. /// /// A lazy reason contains a payload that propagators can use to identify what type of /// propagation the reason is for. The payload should be enough for the propagator to construct - /// an explanation based on its internal state. + /// an explanation based on its internal state. The [`InferenceCode`] is returned by + /// [`crate::propagation::Propagator::lazy_explanation`] on demand. DynamicLazy(u64), } @@ -105,44 +117,145 @@ pub enum Reason { #[derive(Debug, Clone)] pub(crate) enum StoredReason { /// An eager reason contains the propositional conjunction with the reason, without the - /// propagated predicate. - Eager(PropositionalConjunction), + /// propagated predicate, and the [`InferenceCode`] identifying the explanation algorithm. + Eager(PropositionalConjunction, InferenceCode), /// A lazy reason, which is computed on-demand rather than up-front. This is also referred to /// as a 'backward' reason. /// /// A lazy reason contains a payload that propagators can use to identify what type of /// propagation the reason is for. The payload should be enough for the propagator to construct - /// an explanation based on its internal state. + /// an explanation based on its internal state. The [`InferenceCode`] is returned by + /// [`crate::propagation::Propagator::lazy_explanation`] on demand. DynamicLazy(u64), } impl StoredReason { - /// Evaluate the reason, and write the predicates to the `destination_buffer`. + /// Evaluate the reason, write the predicates to `destination_buffer`, and return the + /// [`InferenceCode`] associated with the reason. pub(crate) fn compute( &self, context: ExplanationContext<'_>, propagator_id: PropagatorId, propagators: &mut PropagatorStore, destination_buffer: &mut impl Extend, - ) { + predicate_to_explain: Predicate, + ) -> InferenceCode { match self { // We do not replace the reason with an eager explanation for dynamic lazy explanations. // // Benchmarking will have to show whether this should change or not. - StoredReason::DynamicLazy(code) => destination_buffer.extend( - propagators[propagator_id] - .lazy_explanation(*code, context) - .iter() - .copied(), + StoredReason::DynamicLazy(code) => self.compute_lazy_explanation( + context, + *code, + &mut propagators[propagator_id], + destination_buffer, + predicate_to_explain, ), - StoredReason::Eager(result) => destination_buffer.extend(result.iter().copied()), + + StoredReason::Eager(result, inference_code) => { + destination_buffer.extend(result.iter().copied()); + inference_code.clone() + } + } + } + + fn compute_lazy_explanation( + &self, + mut context: ExplanationContext<'_>, + code: u64, + propagator: &mut dyn Propagator, + destination_buffer: &mut impl Extend, + predicate_to_explain: Predicate, + ) -> InferenceCode { + if let Some((hypercube, linear, inference_code)) = + propagator.explain_as_hypercube_linear(code, context.reborrow()) + { + convert_hl_to_clause( + context, + hypercube, + linear, + destination_buffer, + predicate_to_explain, + ); + inference_code + } else { + let explanation = propagator.lazy_explanation(code, context); + destination_buffer.extend(explanation.predicates.iter().copied()); + explanation.inference_code } } } -impl From for Reason { - fn from(value: PropositionalConjunction) -> Self { - Reason::Eager(value) +fn convert_hl_to_clause( + context: ExplanationContext<'_>, + hypercube: crate::hypercube_linear::Hypercube, + linear: crate::hypercube_linear::LinearInequality, + destination_buffer: &mut impl Extend, + predicate_to_explain: Predicate, +) { + let unsatisfied_hypercube_predicates = hypercube + .iter_predicates() + .filter(|&p| { + context.evaluate_predicate_at_trail_position(p, context.get_trail_position()) + != Some(true) + }) + .collect::>(); + + if unsatisfied_hypercube_predicates.is_empty() { + // The propagation is part of the linear. The hypercube is entirely part of the + // reason. + destination_buffer.extend(hypercube.iter_predicates()); + + // Add all lower bounds, except for the domain that was propagated. The iterator is + // guaranteed to yield every domain at most once. + destination_buffer.extend(linear.terms().filter_map(|term| { + if term.inner == predicate_to_explain.get_domain() { + None + } else { + let lb = context.lower_bound_at_trail_position(&term, context.get_trail_position()); + Some(predicate![term >= lb]) + } + })); + } else { + assert_eq!( + unsatisfied_hypercube_predicates.len(), + 1, + "cannot have more than one unassigned predicate when a hypercube linear propagates" + ); + + let unsatisfied_predicate = unsatisfied_hypercube_predicates[0]; + + // Add all true predicates in the hypercube. + destination_buffer.extend( + hypercube + .iter_predicates() + .filter(|&p| p != unsatisfied_predicate), + ); + + // Add all lower bounds that are true. + destination_buffer.extend( + linear + .terms() + .filter_map(|term| { + if term.inner == predicate_to_explain.get_domain() { + None + } else { + let lb = context + .lower_bound_at_trail_position(&term, context.get_trail_position()); + Some(predicate![term >= lb]) + } + }) + .filter(|&p| { + context.evaluate_predicate_at_trail_position(p, context.get_trail_position()) + == Some(true) + }), + ); + } +} + +impl From<(PropositionalConjunction, &InferenceCode)> for Reason { + fn from((conj, code): (PropositionalConjunction, &InferenceCode)) -> Self { + Reason::Eager(conj, code.clone()) } } @@ -178,11 +291,18 @@ impl Slot<'_> { #[cfg(test)] mod tests { + use std::num::NonZero; + use super::*; use crate::conjunction; use crate::engine::Assignments; use crate::engine::notifications::NotificationEngine; use crate::engine::variables::DomainId; + use crate::proof::ConstraintTag; + + fn dummy_inference_code() -> InferenceCode { + InferenceCode::unknown_label(ConstraintTag::from_non_zero(NonZero::new(1).unwrap())) + } #[test] fn computing_an_eager_reason_returns_a_reference_to_the_conjunction() { @@ -193,14 +313,15 @@ mod tests { let y = DomainId::new(1); let conjunction = conjunction!([x == 1] & [y == 2]); - let reason = StoredReason::Eager(conjunction.clone()); + let reason = StoredReason::Eager(conjunction.clone(), dummy_inference_code()); let mut out_reason = vec![]; - reason.compute( + let _ = reason.compute( ExplanationContext::test_new(&integers, &mut notification_engine), PropagatorId(0), &mut PropagatorStore::default(), &mut out_reason, + predicate![x == 5], ); assert_eq!(conjunction.as_slice(), &out_reason); @@ -216,8 +337,10 @@ mod tests { let y = DomainId::new(1); let conjunction = conjunction!([x == 1] & [y == 2]); - let reason_ref = - reason_store.push(PropagatorId(0), StoredReason::Eager(conjunction.clone())); + let reason_ref = reason_store.push( + PropagatorId(0), + StoredReason::Eager(conjunction.clone(), dummy_inference_code()), + ); assert_eq!(ReasonRef(0), reason_ref); @@ -227,6 +350,7 @@ mod tests { ExplanationContext::test_new(&integers, &mut notification_engine), &mut PropagatorStore::default(), &mut out_reason, + predicate![x == 5], ); assert_eq!(conjunction.as_slice(), &out_reason); diff --git a/pumpkin-crates/core/src/engine/cp/test_solver.rs b/pumpkin-crates/core/src/engine/cp/test_solver.rs index 4bbcdf795..a852e44bb 100644 --- a/pumpkin-crates/core/src/engine/cp/test_solver.rs +++ b/pumpkin-crates/core/src/engine/cp/test_solver.rs @@ -300,6 +300,7 @@ impl TestSolver { ), &mut self.state.propagators, &mut predicates, + predicate, ); PropositionalConjunction::from(predicates) diff --git a/pumpkin-crates/core/src/engine/debug_helper.rs b/pumpkin-crates/core/src/engine/debug_helper.rs index d3eae77ec..b1e30e8f0 100644 --- a/pumpkin-crates/core/src/engine/debug_helper.rs +++ b/pumpkin-crates/core/src/engine/debug_helper.rs @@ -3,6 +3,7 @@ use std::fmt::Formatter; use std::iter::once; use log::debug; +use log::trace; use super::TrailedValues; use super::notifications::NotificationEngine; @@ -53,6 +54,7 @@ impl DebugHelper { propagators: &PropagatorStore, notification_engine: &NotificationEngine, ) -> bool { + trace!("---------- starting fixed point check"); let mut assignments_clone = assignments.clone(); let mut trailed_values_clone = trailed_values.clone(); let mut notification_engine_clone = @@ -114,6 +116,9 @@ impl DebugHelper { panic!("missed propagations"); } } + + trace!("---------- end fixed point check"); + true } @@ -151,6 +156,7 @@ impl DebugHelper { propagators: &mut PropagatorStore, notification_engine: &NotificationEngine, ) -> bool { + trace!("---------- start propagation check"); if propagators .as_propagator_handle::(propagator_id) .is_some() @@ -169,8 +175,7 @@ impl DebugHelper { let _ = reason_store.get_or_compute( trail_entry .reason - .expect("Expected checked propagation to have a reason") - .0, + .expect("Expected checked propagation to have a reason"), ExplanationContext::without_working_nogood( assignments, trail_index, @@ -178,6 +183,7 @@ impl DebugHelper { ), propagators, &mut reason, + trail_entry.predicate, ); result &= Self::debug_propagator_reason( @@ -190,6 +196,7 @@ impl DebugHelper { notification_engine, ); } + trace!("---------- end propagation check"); result } diff --git a/pumpkin-crates/core/src/engine/literal_block_distance.rs b/pumpkin-crates/core/src/engine/literal_block_distance.rs index 057f90bbc..8d3887afe 100644 --- a/pumpkin-crates/core/src/engine/literal_block_distance.rs +++ b/pumpkin-crates/core/src/engine/literal_block_distance.rs @@ -11,12 +11,8 @@ pub struct Lbd { impl Default for Lbd { fn default() -> Self { - fn sparse_set_mapping(elem: &u32) -> usize { - *elem as usize - } - Lbd { - lbd_helper: SparseSet::new(vec![], sparse_set_mapping), + lbd_helper: SparseSet::new(vec![]), } } } diff --git a/pumpkin-crates/core/src/engine/mod.rs b/pumpkin-crates/core/src/engine/mod.rs index c3854bec0..22da37fbe 100644 --- a/pumpkin-crates/core/src/engine/mod.rs +++ b/pumpkin-crates/core/src/engine/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod cp; mod debug_helper; mod literal_block_distance; pub(crate) mod notifications; +mod predicate_heap; pub(crate) mod predicates; mod restart_strategy; mod solver_statistics; @@ -22,6 +23,7 @@ pub(crate) use cp::*; pub(crate) use debug_helper::DebugDyn; pub(crate) use debug_helper::DebugHelper; pub use literal_block_distance::Lbd; +pub use predicate_heap::*; pub use reason::Reason; pub use restart_strategy::RestartOptions; pub(crate) use restart_strategy::RestartStrategy; diff --git a/pumpkin-crates/core/src/engine/notifications/mod.rs b/pumpkin-crates/core/src/engine/notifications/mod.rs index ba6a5e64e..e46faf232 100644 --- a/pumpkin-crates/core/src/engine/notifications/mod.rs +++ b/pumpkin-crates/core/src/engine/notifications/mod.rs @@ -169,6 +169,12 @@ impl NotificationEngine { trailed_values: &mut TrailedValues, assignments: &Assignments, ) { + // If the predicate is trivially true, then the propagator will never be notified. + // Therefore, it makes no sense to track it. + if assignments.is_initial_bound(self.get_predicate(predicate_id)) { + return; + } + self.watch_list_predicate_id .accomodate(predicate_id, vec![]); self.watch_list_predicate_id[predicate_id].push(propagator_id); @@ -181,7 +187,13 @@ impl NotificationEngine { &mut self, predicate_id: PredicateId, propagator_to_unwatch: PropagatorId, + assignments: &Assignments, ) { + // If the predicate is an initial bound, then it was never watched to begin with. + if assignments.is_initial_bound(self.get_predicate(predicate_id)) { + return; + } + let watch_list = &mut self.watch_list_predicate_id[predicate_id]; let index = watch_list @@ -399,6 +411,8 @@ impl NotificationEngine { let enqueue_decision = propagator.notify_predicate_id_satisfied(context.reborrow(), predicate_id); + // trace!("notifying {propagator_id:?}"); + if enqueue_decision == EnqueueDecision::Enqueue { propagator_queue.enqueue_propagator(propagator_id, propagator.priority()); } @@ -419,6 +433,8 @@ impl NotificationEngine { ) { let context = NotificationContext::new(trailed_values, assignments); + // trace!("notifying {propagator_id:?}"); + let enqueue_decision = propagators[propagator_id].notify(context, local_id, event.into()); if enqueue_decision == EnqueueDecision::Enqueue { diff --git a/pumpkin-crates/core/src/engine/notifications/predicate_notification/predicate_notifier.rs b/pumpkin-crates/core/src/engine/notifications/predicate_notification/predicate_notifier.rs index feefdc71b..16ead95ad 100644 --- a/pumpkin-crates/core/src/engine/notifications/predicate_notification/predicate_notifier.rs +++ b/pumpkin-crates/core/src/engine/notifications/predicate_notification/predicate_notifier.rs @@ -114,10 +114,8 @@ impl PredicateNotifier { let predicate = self.predicate_to_id.get_predicate(id); // First, we resize the number of DomainIds for which we store predicate trackers - if self.domain_id_to_predicate_tracker.len() <= predicate.get_domain().index() { - self.domain_id_to_predicate_tracker - .resize(predicate.get_domain().index() + 1, PredicateTracker::new()); - } + self.domain_id_to_predicate_tracker + .accomodate(predicate.get_domain(), PredicateTracker::new()); // Now we initialise the predicate tracker; this does not add it to the scope yet but it // initialises the structures diff --git a/pumpkin-crates/core/src/engine/notifications/predicate_notification/predicate_tracker.rs b/pumpkin-crates/core/src/engine/notifications/predicate_notification/predicate_tracker.rs index 12da90c00..d1ff83907 100644 --- a/pumpkin-crates/core/src/engine/notifications/predicate_notification/predicate_tracker.rs +++ b/pumpkin-crates/core/src/engine/notifications/predicate_notification/predicate_tracker.rs @@ -265,10 +265,12 @@ impl PredicateTracker { /// /// If the index is out of bounds, this method will panic. fn get_value_at_index(&self, index: usize) -> TrackedValue { - *self - .values - .get_index(index) - .expect("Expected provided index to exist") + self.values.get_index(index).copied().unwrap_or_else(|| { + panic!( + "index out of bounds: len is {} and index is {index}", + self.values.len() + ); + }) } /// Returns all of the values currently present. @@ -1087,7 +1089,7 @@ mod tests { vec![ PredicateType::LowerBound, PredicateType::NotEqual, - PredicateType::Equal + PredicateType::Equal, ] ); assert_eq!(value.get_value(), x); @@ -1097,9 +1099,9 @@ mod tests { value.get_predicate_types().collect::>(), vec![ PredicateType::LowerBound, - PredicateType::UpperBound, PredicateType::NotEqual, - PredicateType::Equal + PredicateType::Equal, + PredicateType::UpperBound, ] ); assert_eq!(value.get_value(), x); @@ -1137,7 +1139,7 @@ mod tests { vec![ PredicateType::LowerBound, PredicateType::NotEqual, - PredicateType::Equal + PredicateType::Equal, ] ); assert_eq!(value.get_value(), x); @@ -1147,9 +1149,9 @@ mod tests { value.get_predicate_types().collect::>(), vec![ PredicateType::LowerBound, - PredicateType::UpperBound, PredicateType::NotEqual, - PredicateType::Equal + PredicateType::Equal, + PredicateType::UpperBound, ] ); assert_eq!(value.get_value(), x); diff --git a/pumpkin-crates/core/src/engine/predicate_heap.rs b/pumpkin-crates/core/src/engine/predicate_heap.rs new file mode 100644 index 000000000..83ffe5e2d --- /dev/null +++ b/pumpkin-crates/core/src/engine/predicate_heap.rs @@ -0,0 +1,70 @@ +use std::cmp::Ordering; +use std::collections::BinaryHeap; + +use crate::predicates::Predicate; +use crate::state::State; + +/// A max-heap of predicates. The keys are based on the trail positions of the predicates in the +/// state, meaning predicates are popped in reverse trail order. Implied predicates are popped +/// before the predicate on the trail that implies the predicate. +#[derive(Clone, Debug, Default)] +pub struct PredicateHeap { + heap: BinaryHeap, +} + +impl PredicateHeap { + /// See [`BinaryHeap::is_empty`]. + pub fn is_empty(&self) -> bool { + self.heap.is_empty() + } + + /// See [`BinaryHeap::pop`]. + pub fn pop(&mut self) -> Option { + self.heap.pop().map(|to_explain| to_explain.predicate) + } + + /// Push a new predicate onto the heap. + /// + /// Its priority will be based on its trail position in the given `state`. This heap will + /// return elements through [`Self::pop`] by reverse-trail order. + /// + /// If the predicate is not true in the given state, this method panics. + pub fn push(&mut self, predicate: Predicate, state: &State) { + let trail_position = state + .trail_position(predicate) + .expect("predicate must be true in given state"); + + let priority = if state.is_on_trail(predicate) { + trail_position * 2 + } else { + trail_position * 2 + 1 + }; + + self.heap.push(PredicateToExplain { + predicate, + priority, + }); + } +} + +/// Used to order the predicates in the [`PredicateHeap`]. +/// +/// The priority is calculated based on the trail position of the predicate and whether the +/// predicate is on the trail or implied. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct PredicateToExplain { + predicate: Predicate, + priority: usize, +} + +impl PartialOrd for PredicateToExplain { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PredicateToExplain { + fn cmp(&self, other: &Self) -> Ordering { + self.priority.cmp(&other.priority) + } +} diff --git a/pumpkin-crates/core/src/engine/predicates/predicate.rs b/pumpkin-crates/core/src/engine/predicates/predicate.rs index df6edac0f..f488b9279 100644 --- a/pumpkin-crates/core/src/engine/predicates/predicate.rs +++ b/pumpkin-crates/core/src/engine/predicates/predicate.rs @@ -10,6 +10,16 @@ use crate::propagation::DomainEvent; /// ([`DomainId`], [`PredicateType`], value). /// /// To create a [`Predicate`], use [Predicate::new] or the more concise [predicate!] macro. +/// +/// ## Order +/// Predicates have a well-defined order. They are first ordered by the domain, and then by +/// predicate type, and finally by the value. The order is chosen such that for a fixed domain `x`, +/// predicates are ordered as follows: +/// [>= 5], [>= 7], [!= 2], [!= 3], [== 5], [!= 7], [<= 6], [<= 10] +/// +/// From the order, we get the lower-bound predicates first, ordered by non-decreasing bound, then +/// the (not-)equal predicates, ordered by non-decreasing bound, then the upper-bound predicates, +/// ordered by non-increasing bounds. #[derive(Clone, PartialEq, Eq, Copy, Hash)] pub struct Predicate { /// The two most significant bits of the id stored in the [`Predicate`] contains the type of @@ -18,25 +28,72 @@ pub struct Predicate { value: i32, } -const LOWER_BOUND_CODE: u8 = 0; -const UPPER_BOUND_CODE: u8 = 1; -const NOT_EQUAL_CODE: u8 = 2; -const EQUAL_CODE: u8 = 3; +const LOWER_BOUND_CODE: u8 = PredicateType::LowerBound as u8; +const UPPER_BOUND_CODE: u8 = PredicateType::UpperBound as u8; +const NOT_EQUAL_CODE: u8 = PredicateType::NotEqual as u8; +const EQUAL_CODE: u8 = PredicateType::Equal as u8; impl Predicate { /// Creates a new [`Predicate`] (also known as atomic constraint) which represents a domain /// operation. pub fn new(id: DomainId, predicate_type: PredicateType, value: i32) -> Self { - let code = match predicate_type { - PredicateType::LowerBound => LOWER_BOUND_CODE, - PredicateType::UpperBound => UPPER_BOUND_CODE, - PredicateType::NotEqual => NOT_EQUAL_CODE, - PredicateType::Equal => EQUAL_CODE, - }; + let code = predicate_type as u8; let id = id.id() | (code as u32) << 30; Self { id, value } } + /// Returns `true` if `self` implies `other`. + /// + /// # Example + /// ``` + /// # use pumpkin_core::variables::DomainId; + /// # use pumpkin_core::predicate; + /// let x = DomainId::new(0); + /// + /// assert!(predicate![x >= 5].implies(predicate![x >= 3])); + /// assert!(predicate![x >= 5].implies(predicate![x != 1])); + /// assert!(predicate![x == 5].implies(predicate![x <= 5])); + /// ``` + pub fn implies(&self, other: Predicate) -> bool { + if self.get_domain() != other.get_domain() { + // Predicates only imply other predicates on the same domain. + return false; + } + + match self.get_predicate_type() { + PredicateType::LowerBound => match other.get_predicate_type() { + PredicateType::LowerBound => { + self.get_right_hand_side() >= other.get_right_hand_side() + } + PredicateType::NotEqual => self.get_right_hand_side() > other.get_right_hand_side(), + PredicateType::UpperBound | PredicateType::Equal => false, + }, + PredicateType::UpperBound => match other.get_predicate_type() { + PredicateType::UpperBound => { + self.get_right_hand_side() <= other.get_right_hand_side() + } + PredicateType::NotEqual => self.get_right_hand_side() < other.get_right_hand_side(), + PredicateType::LowerBound | PredicateType::Equal => false, + }, + PredicateType::NotEqual => { + other.get_predicate_type() == PredicateType::NotEqual + && self.get_right_hand_side() == other.get_right_hand_side() + } + PredicateType::Equal => match other.get_predicate_type() { + PredicateType::LowerBound => { + self.get_right_hand_side() >= other.get_right_hand_side() + } + PredicateType::UpperBound => { + self.get_right_hand_side() <= other.get_right_hand_side() + } + PredicateType::NotEqual => { + self.get_right_hand_side() != other.get_right_hand_side() + } + PredicateType::Equal => self.get_right_hand_side() == other.get_right_hand_side(), + }, + } + } + fn get_type_code(&self) -> u8 { (self.id >> 30) as u8 } @@ -44,6 +101,39 @@ impl Predicate { pub fn get_predicate_type(&self) -> PredicateType { (*self).into() } + + fn is_bound_predicate(&self) -> bool { + self.is_upper_bound_predicate() || self.is_lower_bound_predicate() + } +} + +impl PartialOrd for Predicate { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Predicate { + /// See [`Predicate`] for details on the order. + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let domain_order = self.get_domain().cmp(&other.get_domain()); + + if domain_order != std::cmp::Ordering::Equal { + return domain_order; + } + + if !self.is_bound_predicate() && !other.is_bound_predicate() { + // If neither predicate is a bound predicate, then we order by right-hand side. + return self.get_right_hand_side().cmp(&other.get_right_hand_side()); + } + + match self.get_type_code().cmp(&other.get_type_code()) { + std::cmp::Ordering::Equal => { + self.get_right_hand_side().cmp(&other.get_right_hand_side()) + } + ordering @ (std::cmp::Ordering::Less | std::cmp::Ordering::Greater) => ordering, + } + } } #[derive(Debug, Hash, EnumSetType)] @@ -53,9 +143,9 @@ pub enum PredicateType { // Should correspond with the codes defined previously; `EnumSetType` requires that literals // are used and not expressions LowerBound = 0, - UpperBound = 1, - NotEqual = 2, - Equal = 3, + NotEqual = 1, + Equal = 2, + UpperBound = 3, } impl From for PredicateType { @@ -321,4 +411,68 @@ mod test { let trivially_false = Predicate::trivially_false(); assert!(!trivially_false == trivially_true); } + + #[test] + fn predicates_over_same_domain_are_ordered_by_increasing_lower_bound() { + let x = DomainId::new(0); + let p1 = predicate![x >= 4]; + let p2 = predicate![x >= 6]; + assert!(p1 < p2); + } + + #[test] + fn not_equal_predicates_are_bigger_than_lower_bounds() { + let x = DomainId::new(0); + let p1 = predicate![x >= 4]; + let p2 = predicate![x != 6]; + let p3 = predicate![x != 2]; + + assert!(p1 < p2); + assert!(p1 < p3); + } + + #[test] + fn not_equal_predicates_are_ordered_by_rhs() { + let x = DomainId::new(0); + let p1 = predicate![x != 6]; + let p2 = predicate![x != 2]; + + assert!(p1 > p2); + } + + #[test] + fn equal_predicates_are_ordered_by_rhs() { + let x = DomainId::new(0); + let p1 = predicate![x == 6]; + let p2 = predicate![x == 2]; + + assert!(p1 > p2); + } + + #[test] + fn equal_predicates_bigger_than_lower_bounds() { + let x = DomainId::new(0); + let p1 = predicate![x == 6]; + let p2 = predicate![x >= 2]; + + assert!(p1 > p2); + } + + #[test] + fn equal_predicates_smaller_than_upper_bounds() { + let x = DomainId::new(0); + let p1 = predicate![x == 6]; + let p2 = predicate![x <= 2]; + + assert!(p1 < p2); + } + + #[test] + fn tighter_upper_bound_is_smaller() { + let x = DomainId::new(0); + let p1 = predicate![x <= 6]; + let p2 = predicate![x <= 2]; + + assert!(p1 > p2); + } } diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs index 998741fb4..8447ff623 100644 --- a/pumpkin-crates/core/src/engine/state.rs +++ b/pumpkin-crates/core/src/engine/state.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use log::trace; use pumpkin_checking::BoxedChecker; use pumpkin_checking::InferenceChecker; #[cfg(feature = "check-propagations")] @@ -12,15 +13,17 @@ use crate::engine::Assignments; use crate::engine::ConstraintProgrammingTrailEntry; use crate::engine::DebugHelper; use crate::engine::PropagatorQueue; -#[cfg(test)] -use crate::engine::Reason; use crate::engine::TrailedValues; use crate::engine::VariableNames; +#[cfg(test)] +use crate::engine::cp::reason::StoredReason; use crate::engine::notifications::NotificationEngine; use crate::engine::reason::ReasonStore; use crate::predicate; use crate::predicates::Predicate; use crate::predicates::PredicateType; +#[cfg(test)] +use crate::predicates::PropositionalConjunction; use crate::proof::ConstraintTag; use crate::proof::InferenceCode; use crate::propagation::CurrentNogood; @@ -446,7 +449,6 @@ impl State { .post_predicate(predicate, None, &mut self.notification_engine) .map_err(|_| EmptyDomainConflict { trigger_predicate: predicate, - trigger_inference_code: None, trigger_reason: None, }) } @@ -455,7 +457,7 @@ impl State { fn post_with_reason( &mut self, predicate: Predicate, - reason: impl Into, + reason: PropositionalConjunction, inference_code: InferenceCode, propagator_id: PropagatorId, ) -> Result<(), EmptyDomainConflict> { @@ -463,29 +465,24 @@ impl State { let modification_result = self.assignments.post_predicate( predicate, - Some((slot.reason_ref(), inference_code.clone())), + Some(slot.reason_ref()), &mut self.notification_engine, ); match modification_result { Ok(false) => Ok(()), Ok(true) => { - use crate::propagation::build_reason; - - let _ = slot.populate(propagator_id, build_reason(reason, None)); + let _ = slot.populate(propagator_id, StoredReason::Eager(reason, inference_code)); Ok(()) } Err(_) => { - use crate::propagation::build_reason; - - let _ = slot.populate(propagator_id, build_reason(reason, None)); - let (trigger_predicate, trigger_reason, trigger_inference_code) = + let _ = slot.populate(propagator_id, StoredReason::Eager(reason, inference_code)); + let (trigger_predicate, trigger_reason) = self.assignments.remove_last_trail_element(); Err(EmptyDomainConflict { trigger_predicate, trigger_reason: Some(trigger_reason), - trigger_inference_code: Some(trigger_inference_code), }) } } @@ -609,6 +606,7 @@ impl State { let propagation_status = { let propagator = &mut self.propagators[propagator_id]; + trace!("propagating {propagator_id:?} ({})", propagator.name()); let context = PropagationContext::new( &mut self.trailed_values, &mut self.assignments, @@ -642,7 +640,8 @@ impl State { &mut self.propagators, &self.notification_engine ), - "Checking the propagations performed by the propagator led to inconsistencies!" + "Checking the propagations performed by the propagator led to + inconsistencies!" ); } Err(conflict) => { @@ -651,6 +650,11 @@ impl State { self.statistics.num_conflicts += 1; if let Conflict::Propagator(inner) = &conflict { + trace!( + "propagated conflict by {propagator_id} @ {dl}", + dl = self.get_checkpoint() + ); + pumpkin_assert_advanced!(DebugHelper::debug_reported_failure( &self.trailed_values, &self.assignments, @@ -698,12 +702,12 @@ impl State { for trail_index in first_propagation_index..self.assignments.num_trail_entries() { let entry = self.assignments.get_trail_entry(trail_index); - let (reason_ref, inference_code) = entry + let reason_ref = entry .reason .expect("propagations should only be checked after propagations"); reason_buffer.clear(); - let reason_exists = self.reason_store.get_or_compute( + let inference_code = self.reason_store.get_or_compute( reason_ref, ExplanationContext::without_working_nogood( &self.assignments, @@ -712,8 +716,8 @@ impl State { ), &mut self.propagators, &mut reason_buffer, + entry.predicate, ); - assert!(reason_exists, "all propagations have reasons"); self.run_checker( std::mem::take(&mut reason_buffer), @@ -737,6 +741,8 @@ impl State { /// Once the [`State`] is conflicting, then the only operation that is defined is /// [`State::restore_to`]. All other operations and queries on the state are unspecified. pub fn propagate_to_fixed_point(&mut self) -> Result<(), Conflict> { + trace!("Propagation to fixedpoint @ {}", self.get_checkpoint()); + // The initial domain events are due to the decision predicate. self.notification_engine .notify_propagators_about_domain_events( @@ -826,12 +832,12 @@ impl State { &mut self, trail_position: usize, reason_buffer: &mut (impl Extend + AsRef<[Predicate]>), - ) { + ) -> InferenceCode { let entry = self.trail_entry(trail_position); - let (reason_ref, _) = entry + let reason_ref = entry .reason .expect("Added by a propagator and must therefore have a reason"); - let _ = self.reason_store.get_or_compute( + self.reason_store.get_or_compute( reason_ref, ExplanationContext::without_working_nogood( &self.assignments, @@ -840,8 +846,10 @@ impl State { ), &mut self.propagators, reason_buffer, - ); + entry.predicate, + ) } + /// Get the reason for a predicate being true and store it in `reason_buffer`. /// /// If the provided [`Predicate`] is propagated by a propagator, then the [`InferenceCode`] @@ -889,7 +897,7 @@ impl State { // We distinguish between three cases: // 1) The predicate is explicitly present on the trail. if trail_entry.predicate == predicate { - let (reason_ref, inference_code) = trail_entry.reason?; + let reason_ref = trail_entry.reason?; let explanation_context = ExplanationContext::new( &self.assignments, @@ -898,15 +906,14 @@ impl State { &mut self.notification_engine, ); - let reason_exists = self.reason_store.get_or_compute( + let inference_code = self.reason_store.get_or_compute( reason_ref, explanation_context, &mut self.propagators, reason_buffer, + trail_entry.predicate, ); - assert!(reason_exists, "reason reference should not be stale"); - Some(inference_code) } // 2) The predicate is true due to a propagation, and not explicitly on the trail. diff --git a/pumpkin-crates/core/src/engine/variable_names.rs b/pumpkin-crates/core/src/engine/variable_names.rs index 035583404..1dd76478c 100644 --- a/pumpkin-crates/core/src/engine/variable_names.rs +++ b/pumpkin-crates/core/src/engine/variable_names.rs @@ -19,4 +19,11 @@ impl VariableNames { pub(crate) fn add_integer(&mut self, integer: DomainId, name: Arc) { let _ = self.integers.insert(integer, name); } + + /// Get the named domain IDs in the solver. + pub(crate) fn named_domains(&self) -> impl Iterator { + self.integers + .iter() + .map(|(domain_id, name)| (*domain_id, name.as_ref())) + } } diff --git a/pumpkin-crates/core/src/engine/variables/affine_view.rs b/pumpkin-crates/core/src/engine/variables/affine_view.rs index 404184e57..17067f9d5 100644 --- a/pumpkin-crates/core/src/engine/variables/affine_view.rs +++ b/pumpkin-crates/core/src/engine/variables/affine_view.rs @@ -19,9 +19,9 @@ use crate::math::num_ext::NumExt; /// domain of `x`. #[derive(Clone, Copy, Hash, Eq, PartialEq)] pub struct AffineView { - pub(crate) inner: Inner, - pub(crate) scale: i32, - pub(crate) offset: i32, + pub inner: Inner, + pub scale: i32, + pub offset: i32, } impl AffineView { diff --git a/pumpkin-crates/core/src/hypercube_linear/bound_predicate.rs b/pumpkin-crates/core/src/hypercube_linear/bound_predicate.rs new file mode 100644 index 000000000..804604883 --- /dev/null +++ b/pumpkin-crates/core/src/hypercube_linear/bound_predicate.rs @@ -0,0 +1,62 @@ +use std::ops::Not; + +use crate::predicate; +use crate::predicates::Predicate; +use crate::variables::DomainId; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BoundPredicate { + pub domain: DomainId, + pub comparator: BoundComparator, + pub value: i32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BoundComparator { + LowerBound, + UpperBound, +} + +impl BoundPredicate { + pub fn new(predicate: Predicate) -> Option { + let comparator = match predicate.get_predicate_type() { + crate::predicates::PredicateType::LowerBound => BoundComparator::LowerBound, + crate::predicates::PredicateType::UpperBound => BoundComparator::UpperBound, + crate::predicates::PredicateType::NotEqual => return None, + crate::predicates::PredicateType::Equal => return None, + }; + + Some(BoundPredicate { + domain: predicate.get_domain(), + comparator, + value: predicate.get_right_hand_side(), + }) + } +} + +impl From for Predicate { + fn from(value: BoundPredicate) -> Self { + match value.comparator { + BoundComparator::LowerBound => predicate![value.domain >= value.value], + BoundComparator::UpperBound => predicate![value.domain <= value.value], + } + } +} + +impl Not for BoundPredicate { + type Output = BoundPredicate; + + fn not(self) -> Self::Output { + BoundPredicate { + domain: self.domain, + comparator: match self.comparator { + BoundComparator::LowerBound => BoundComparator::UpperBound, + BoundComparator::UpperBound => BoundComparator::LowerBound, + }, + value: match self.comparator { + BoundComparator::LowerBound => self.value - 1, + BoundComparator::UpperBound => self.value + 1, + }, + } + } +} diff --git a/pumpkin-crates/core/src/propagators/hypercube_linear/checker.rs b/pumpkin-crates/core/src/hypercube_linear/checker.rs similarity index 100% rename from pumpkin-crates/core/src/propagators/hypercube_linear/checker.rs rename to pumpkin-crates/core/src/hypercube_linear/checker.rs diff --git a/pumpkin-crates/core/src/hypercube_linear/conflict_state.rs b/pumpkin-crates/core/src/hypercube_linear/conflict_state.rs new file mode 100644 index 000000000..a36a7a062 --- /dev/null +++ b/pumpkin-crates/core/src/hypercube_linear/conflict_state.rs @@ -0,0 +1,105 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use crate::hypercube_linear::Hypercube; +use crate::hypercube_linear::LinearInequality; +use crate::hypercube_linear::Trace; +use crate::hypercube_linear::predicate_heap::PredicateHeap; +use crate::hypercube_linear::trail_view::TrailView; +use crate::hypercube_linear::trail_view::affine_lower_bound_at; +use crate::predicate; +use crate::predicates::Predicate; +use crate::variables::AffineView; +use crate::variables::DomainId; + +#[derive(Clone, Debug)] +pub(crate) struct ConflictState { + pub(crate) working_hypercube: Hypercube, + pub(crate) hypercube_predicates_on_conflict_dl: PredicateHeap, + pub(crate) predicates_to_explain: PredicateHeap, + pub(crate) conflicting_linear: LinearInequality, + pub(crate) proof_file: Rc>, +} + +impl ConflictState { + pub(crate) fn new(proof_file: Rc>) -> Self { + Self { + working_hypercube: Default::default(), + hypercube_predicates_on_conflict_dl: Default::default(), + predicates_to_explain: Default::default(), + conflicting_linear: Default::default(), + proof_file, + } + } + + /// Adds a predicate to the conflicting hypercube. + /// + /// Depending on the checkpoint that the predicate is assigned, the predicate is either + /// explained further or stored as part of the final learned constraint. + pub(crate) fn add_hypercube_predicate(&mut self, trail: &dyn TrailView, predicate: Predicate) { + let checkpoint = trail + .checkpoint_for_predicate(predicate) + .unwrap_or_else(|| panic!("adding unassigned predicate {predicate} to hypercube")); + + #[cfg(feature = "hl-checks")] + assert!( + trail.checkpoint_for_predicate(predicate).is_some(), + "adding untrue predicate {predicate} to hypercube" + ); + + if checkpoint == 0 { + self.proof_file.borrow_mut().axiom([!predicate], [], -1); + } else if checkpoint == trail.current_checkpoint() { + self.predicates_to_explain.push(predicate, trail); + self.hypercube_predicates_on_conflict_dl + .push(predicate, trail); + } else { + self.working_hypercube = std::mem::take(&mut self.working_hypercube) + .with_predicate(predicate) + .expect("cannot create trivially false hypercube"); + } + } + + /// Enqueue the contributions of the linear terms to be explained. + pub(crate) fn explain_linear( + &mut self, + trail: &dyn TrailView, + linear: &LinearInequality, + trail_position: usize, + ) { + for term in linear.terms() { + let term_bound = affine_lower_bound_at(trail, term, trail_position); + let predicate = predicate![term >= term_bound]; + + let checkpoint = trail + .checkpoint_for_predicate(predicate) + .expect("the predicate is true"); + + if checkpoint == trail.current_checkpoint() { + self.predicates_to_explain.push(predicate, trail); + } + } + } + + pub(crate) fn contributes_to_conflict(&self, pivot: Predicate) -> bool { + let is_in_hypercube = self.hypercube_predicates_on_conflict_dl.contains(pivot); + let contributes_to_linear_conflict = self + .conflicting_linear + .term_for_domain(pivot.get_domain()) + .is_some_and(|term| predicate_applies_to_term(pivot, term)); + + is_in_hypercube || contributes_to_linear_conflict + } +} + +pub(super) fn predicate_applies_to_term(pivot: Predicate, term: AffineView) -> bool { + if pivot.get_domain() != term.inner { + return false; + } + + if term.scale.is_positive() { + pivot.is_lower_bound_predicate() + } else { + pivot.is_upper_bound_predicate() + } +} diff --git a/pumpkin-crates/core/src/hypercube_linear/constraint.rs b/pumpkin-crates/core/src/hypercube_linear/constraint.rs new file mode 100644 index 000000000..dc1659eaf --- /dev/null +++ b/pumpkin-crates/core/src/hypercube_linear/constraint.rs @@ -0,0 +1,395 @@ +use std::num::NonZero; + +use crate::ConstraintOperationError; +use crate::Solver; +use crate::constraints::Constraint; +use crate::constraints::NegatableConstraint; +use crate::hypercube_linear::Hypercube; +use crate::hypercube_linear::HypercubeLinearConstructor; +use crate::hypercube_linear::LinearInequality; +use crate::predicates::Predicate; +use crate::proof::ConstraintTag; +use crate::variables::DomainId; +use crate::variables::Literal; + +pub fn hypercube_linear_le( + hypercube: Predicates, + linear_terms: LinearTerms, + linear_rhs: i32, + constraint_tag: ConstraintTag, +) -> impl NegatableConstraint +where + Predicates: IntoIterator + Clone + 'static, + Predicates::IntoIter: Clone, + LinearTerms: IntoIterator, DomainId)> + Clone + 'static, +{ + HLLeConstraint { + hypercube, + linear_terms, + linear_rhs, + constraint_tag, + } +} + +pub fn hypercube_linear_eq( + hypercube: Predicates, + linear_terms: LinearTerms, + linear_rhs: i32, + constraint_tag: ConstraintTag, +) -> impl NegatableConstraint +where + Predicates: IntoIterator + Clone + 'static, + Predicates::IntoIter: Clone, + LinearTerms: IntoIterator, DomainId)> + Clone + 'static, + LinearTerms::IntoIter: Clone, +{ + HLEqConstraint { + hypercube, + linear_terms, + linear_rhs, + constraint_tag, + } +} + +struct HLLeConstraint { + hypercube: Predicates, + linear_terms: LinearTerms, + linear_rhs: i32, + constraint_tag: ConstraintTag, +} + +impl Constraint for HLLeConstraint +where + Predicates: IntoIterator + Clone + 'static, + Predicates::IntoIter: Clone, + LinearTerms: IntoIterator, DomainId)> + Clone + 'static, +{ + fn post(self, solver: &mut Solver) -> Result<(), ConstraintOperationError> { + let Ok(hypercube) = Hypercube::new(self.hypercube) else { + // If the hypercube is inconsistent, then the constraint simplifies to + // `false implies linear`, which is trivially true. + return Ok(()); + }; + + let Some(linear) = LinearInequality::new(self.linear_terms, self.linear_rhs) else { + // If the linear is trivially satisfied, then there is no point in posting + // a constraint. + return Ok(()); + }; + + let _ = solver.add_propagator(HypercubeLinearConstructor { + hypercube, + linear, + constraint_tag: self.constraint_tag, + })?; + + Ok(()) + } + + fn implied_by( + self, + solver: &mut Solver, + reification_literal: Literal, + ) -> Result<(), ConstraintOperationError> { + hypercube_linear_le( + self.hypercube + .into_iter() + .chain(std::iter::once(reification_literal.get_true_predicate())), + self.linear_terms, + self.linear_rhs, + self.constraint_tag, + ) + .post(solver) + } +} + +impl NegatableConstraint for HLLeConstraint +where + Predicates: IntoIterator + Clone + 'static, + Predicates::IntoIter: Clone, + LinearTerms: IntoIterator, DomainId)> + Clone + 'static, +{ + type NegatedConstraint = NotHLLeConstraint; + + fn negation(&self) -> Self::NegatedConstraint { + NotHLLeConstraint { + hypercube: self.hypercube.clone(), + linear_terms: self.linear_terms.clone(), + linear_rhs: self.linear_rhs, + constraint_tag: self.constraint_tag, + } + } +} + +struct NotHLLeConstraint { + hypercube: Predicates, + linear_terms: LinearTerms, + linear_rhs: i32, + constraint_tag: ConstraintTag, +} + +impl Constraint for NotHLLeConstraint +where + Predicates: IntoIterator + Clone + 'static, + Predicates::IntoIter: Clone, + LinearTerms: IntoIterator, DomainId)> + Clone + 'static, +{ + fn post(self, solver: &mut Solver) -> Result<(), ConstraintOperationError> { + for predicate in self.hypercube { + solver.add_clause([predicate], self.constraint_tag)?; + } + + let not_linear_terms = self + .linear_terms + .into_iter() + .map(|(weight, domain)| (-weight, domain)); + let not_linear_rhs = -self.linear_rhs - 1; + + if let Some(not_linear) = LinearInequality::new(not_linear_terms, not_linear_rhs) { + let _ = solver.add_propagator(HypercubeLinearConstructor { + hypercube: Hypercube::default(), + linear: not_linear, + constraint_tag: self.constraint_tag, + })?; + } + + Ok(()) + } + + fn implied_by( + self, + solver: &mut Solver, + reification_literal: Literal, + ) -> Result<(), ConstraintOperationError> { + for predicate in self.hypercube { + solver.add_clause( + [reification_literal.get_false_predicate(), predicate], + self.constraint_tag, + )?; + } + + let not_linear_terms = self + .linear_terms + .into_iter() + .map(|(weight, domain)| (-weight, domain)); + let not_linear_rhs = -self.linear_rhs - 1; + + if let Some(not_linear) = LinearInequality::new(not_linear_terms, not_linear_rhs) { + let _ = solver.add_propagator(HypercubeLinearConstructor { + hypercube: Hypercube::new([reification_literal.get_true_predicate()]) + .expect("single predicate hypercube cannot be inconsistent"), + linear: not_linear, + constraint_tag: self.constraint_tag, + })?; + } + + Ok(()) + } +} + +impl NegatableConstraint for NotHLLeConstraint +where + Predicates: IntoIterator + Clone + 'static, + Predicates::IntoIter: Clone, + LinearTerms: IntoIterator, DomainId)> + Clone + 'static, +{ + type NegatedConstraint = HLLeConstraint; + + fn negation(&self) -> Self::NegatedConstraint { + HLLeConstraint { + hypercube: self.hypercube.clone(), + linear_terms: self.linear_terms.clone(), + linear_rhs: self.linear_rhs, + constraint_tag: self.constraint_tag, + } + } +} + +struct HLEqConstraint { + hypercube: Predicates, + linear_terms: LinearTerms, + linear_rhs: i32, + constraint_tag: ConstraintTag, +} + +impl Constraint for HLEqConstraint +where + Predicates: IntoIterator + Clone + 'static, + Predicates::IntoIter: Clone, + LinearTerms: IntoIterator, DomainId)> + Clone + 'static, + LinearTerms::IntoIter: Clone, +{ + fn post(self, solver: &mut Solver) -> Result<(), ConstraintOperationError> { + hypercube_linear_le( + self.hypercube.clone(), + self.linear_terms.clone(), + self.linear_rhs, + self.constraint_tag, + ) + .post(solver)?; + + let negated_terms = self + .linear_terms + .into_iter() + .map(|(weight, domain)| (-weight, domain)); + + hypercube_linear_le( + self.hypercube.clone(), + negated_terms, + -self.linear_rhs, + self.constraint_tag, + ) + .post(solver)?; + + Ok(()) + } + + fn implied_by( + self, + solver: &mut Solver, + reification_literal: Literal, + ) -> Result<(), ConstraintOperationError> { + hypercube_linear_eq( + self.hypercube + .into_iter() + .chain(std::iter::once(reification_literal.get_true_predicate())), + self.linear_terms, + self.linear_rhs, + self.constraint_tag, + ) + .post(solver) + } +} + +impl NegatableConstraint for HLEqConstraint +where + Predicates: IntoIterator + Clone + 'static, + Predicates::IntoIter: Clone, + LinearTerms: IntoIterator, DomainId)> + Clone + 'static, + LinearTerms::IntoIter: Clone, +{ + type NegatedConstraint = NotHLEqConstraint; + + fn negation(&self) -> Self::NegatedConstraint { + NotHLEqConstraint { + hypercube: self.hypercube.clone(), + linear_terms: self.linear_terms.clone(), + linear_rhs: self.linear_rhs, + constraint_tag: self.constraint_tag, + } + } +} + +struct NotHLEqConstraint { + hypercube: Predicates, + linear_terms: LinearTerms, + linear_rhs: i32, + constraint_tag: ConstraintTag, +} + +impl Constraint for NotHLEqConstraint +where + Predicates: IntoIterator + Clone + 'static, + Predicates::IntoIter: Clone, + LinearTerms: IntoIterator, DomainId)> + Clone + 'static, + LinearTerms::IntoIter: Clone, +{ + fn post(self, solver: &mut Solver) -> Result<(), ConstraintOperationError> { + for predicate in self.hypercube { + solver.add_clause([predicate], self.constraint_tag)?; + } + + // We model the Ax != b as follows (where l is a fresh 0-1 variable): + // l -> Ax < b + // !l -> Ax > b + let l = solver.new_literal(); + + hypercube_linear_le( + [l.get_true_predicate()], + self.linear_terms.clone(), + self.linear_rhs - 1, + self.constraint_tag, + ) + .post(solver)?; + + let not_linear_terms = self + .linear_terms + .into_iter() + .map(|(weight, domain)| (-weight, domain)); + let not_linear_rhs = -self.linear_rhs - 1; + + hypercube_linear_le( + [l.get_false_predicate()], + not_linear_terms, + not_linear_rhs, + self.constraint_tag, + ) + .post(solver)?; + + Ok(()) + } + + fn implied_by( + self, + solver: &mut Solver, + reification_literal: Literal, + ) -> Result<(), ConstraintOperationError> { + for predicate in self.hypercube { + solver.add_clause( + [reification_literal.get_false_predicate(), predicate], + self.constraint_tag, + )?; + } + + let l = solver.new_literal(); + + hypercube_linear_le( + [ + reification_literal.get_true_predicate(), + l.get_true_predicate(), + ], + self.linear_terms.clone(), + self.linear_rhs - 1, + self.constraint_tag, + ) + .post(solver)?; + + let not_linear_terms = self + .linear_terms + .into_iter() + .map(|(weight, domain)| (-weight, domain)); + let not_linear_rhs = -self.linear_rhs - 1; + + hypercube_linear_le( + [ + reification_literal.get_true_predicate(), + l.get_false_predicate(), + ], + not_linear_terms, + not_linear_rhs, + self.constraint_tag, + ) + .post(solver)?; + + Ok(()) + } +} + +impl NegatableConstraint for NotHLEqConstraint +where + Predicates: IntoIterator + Clone + 'static, + Predicates::IntoIter: Clone, + LinearTerms: IntoIterator, DomainId)> + Clone + 'static, + LinearTerms::IntoIter: Clone, +{ + type NegatedConstraint = HLEqConstraint; + + fn negation(&self) -> Self::NegatedConstraint { + HLEqConstraint { + hypercube: self.hypercube.clone(), + linear_terms: self.linear_terms.clone(), + linear_rhs: self.linear_rhs, + constraint_tag: self.constraint_tag, + } + } +} diff --git a/pumpkin-crates/core/src/hypercube_linear/explanation.rs b/pumpkin-crates/core/src/hypercube_linear/explanation.rs new file mode 100644 index 000000000..82aedb9c5 --- /dev/null +++ b/pumpkin-crates/core/src/hypercube_linear/explanation.rs @@ -0,0 +1,289 @@ +use std::fmt::Display; + +use itertools::Itertools; + +use crate::hypercube_linear::BoundPredicate; +use crate::hypercube_linear::Hypercube; +use crate::hypercube_linear::InconsistentHypercube; +use crate::hypercube_linear::LinearInequality; +use crate::hypercube_linear::trail_view::TrailView; +use crate::hypercube_linear::trail_view::affine_lower_bound_at; +use crate::predicate; +use crate::predicates::Predicate; +use crate::variables::AffineView; +use crate::variables::DomainId; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HypercubeLinear { + pub hypercube: Hypercube, + pub linear: LinearInequality, +} + +impl From for HypercubeLinear { + fn from(linear: LinearInequality) -> Self { + HypercubeLinear { + hypercube: Hypercube::default(), + linear, + } + } +} + +impl Display for HypercubeLinear { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} -> {}", self.hypercube, self.linear,) + } +} + +impl HypercubeLinear { + pub fn from_clause( + predicates: impl IntoIterator, + ) -> Result { + let hypercube = Hypercube::new(predicates)?; + + Ok(HypercubeLinear { + hypercube, + linear: LinearInequality::trivially_false(), + }) + } +} + +#[derive(Clone)] +pub(crate) enum HypercubeLinearExplanation { + Proper(HypercubeLinear), + Conjunction(Vec), +} + +impl From for HypercubeLinearExplanation { + fn from(hypercube_linear: HypercubeLinear) -> Self { + HypercubeLinearExplanation::Proper(hypercube_linear) + } +} + +impl From for HypercubeLinearExplanation { + fn from(linear: LinearInequality) -> Self { + HypercubeLinearExplanation::Proper(HypercubeLinear::from(linear)) + } +} + +impl>> From for HypercubeLinearExplanation { + fn from(conjunction: T) -> Self { + HypercubeLinearExplanation::Conjunction(conjunction.into()) + } +} + +impl Default for HypercubeLinearExplanation { + fn default() -> Self { + HypercubeLinearExplanation::Conjunction(vec![]) + } +} + +impl HypercubeLinearExplanation { + pub(crate) fn terms(&self) -> impl Iterator> { + match self { + HypercubeLinearExplanation::Proper(hypercube_linear) => { + itertools::Either::Left(hypercube_linear.linear.terms()) + } + HypercubeLinearExplanation::Conjunction(_) => { + itertools::Either::Right(std::iter::empty()) + } + } + } + + pub(crate) fn is_clause(&self) -> bool { + self.linear() + .is_none_or(|linear| linear.is_trivially_false()) + } + + pub(crate) fn into_clause( + self, + trail: &T, + pivot: Predicate, + trail_position: usize, + ) -> Vec { + let hypercube_linear = match self { + HypercubeLinearExplanation::Proper(hypercube_linear) => hypercube_linear, + HypercubeLinearExplanation::Conjunction(predicates) => return predicates, + }; + + let mut clause = vec![!pivot]; + + clause.extend( + hypercube_linear + .hypercube + .iter_predicates() + // Any predicate in the hypercube that is implied by !pivot should be + // excluded. This is due to the propagation rule that allows a weaker + // hypercube predicate to be propagated by the linear. + .filter(|&p| !(!pivot).implies(p)), + ); + + clause.extend(hypercube_linear.linear.terms().map(|term| { + let term_bound = affine_lower_bound_at(trail, term, trail_position - 1); + predicate![term >= term_bound] + })); + + clause + } + + pub(crate) fn weaken_to_zero(self, bound: BoundPredicate) -> Option { + match self { + HypercubeLinearExplanation::Proper(mut hypercube_linear) => { + hypercube_linear.linear = + std::mem::take(&mut hypercube_linear.linear).weaken_to_zero(bound)?; + + hypercube_linear.hypercube = std::mem::take(&mut hypercube_linear.hypercube) + .with_predicate(bound.into()) + .expect("should never construct inconsistent hypercube"); + + Some(HypercubeLinearExplanation::Proper(hypercube_linear)) + } + + HypercubeLinearExplanation::Conjunction(predicates) => { + Some(HypercubeLinearExplanation::Conjunction(predicates)) + } + } + } + + pub(crate) fn iter_predicates(&self) -> impl Iterator { + match self { + HypercubeLinearExplanation::Proper(hypercube_linear) => { + itertools::Either::Left(hypercube_linear.hypercube.iter_predicates()) + } + HypercubeLinearExplanation::Conjunction(predicates) => { + itertools::Either::Right(predicates.iter().copied()) + } + } + } + + pub(crate) fn take_linear(&mut self) -> LinearInequality { + match self { + HypercubeLinearExplanation::Proper(hypercube_linear) => { + std::mem::take(&mut hypercube_linear.linear) + } + HypercubeLinearExplanation::Conjunction(_) => LinearInequality::trivially_false(), + } + } + + pub(crate) fn linear(&self) -> Option<&LinearInequality> { + match self { + HypercubeLinearExplanation::Proper(hypercube_linear) => Some(&hypercube_linear.linear), + HypercubeLinearExplanation::Conjunction(_) => None, + } + } +} + +impl Display for HypercubeLinearExplanation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + HypercubeLinearExplanation::Proper(hypercube_linear) => { + write!(f, "{hypercube_linear}") + } + HypercubeLinearExplanation::Conjunction(predicates) => { + write!(f, "{} -> <= -1", predicates.iter().format(" & ")) + } + } + } +} + +#[cfg(test)] +mod tests { + use std::num::NonZero; + + use super::*; + use crate::state::State; + + #[test] + fn weaken_to_zero_conjunction_is_passthrough() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + let y = state.new_interval_variable(0, 10, Some("y".into())); + + let predicates = vec![predicate![x >= 3], predicate![y >= 2]]; + let explanation = HypercubeLinearExplanation::Conjunction(predicates.clone()); + + let bound = BoundPredicate::new(predicate![x >= 3]).expect("bound predicate"); + let result = explanation + .weaken_to_zero(bound) + .expect("not trivially satisfiable"); + + let HypercubeLinearExplanation::Conjunction(result_predicates) = result else { + panic!("expected Conjunction variant"); + }; + + assert_eq!(result_predicates, predicates); + } + + #[test] + fn weaken_to_zero_proper_removes_term_and_tightens_hypercube() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + let y = state.new_interval_variable(0, 10, Some("y".into())); + + // x + y <= 5 + let linear = LinearInequality::new( + [(NonZero::new(1).unwrap(), x), (NonZero::new(1).unwrap(), y)], + 5, + ) + .expect("not trivially satisfiable"); + + let explanation = HypercubeLinearExplanation::Proper(HypercubeLinear { + hypercube: Hypercube::new([]).expect("not inconsistent"), + linear, + }); + + // Weaken on x >= 3: removes x from linear (bound becomes 5 - 1*3 = 2). + let bound = BoundPredicate::new(predicate![x >= 3]).expect("bound predicate"); + let result = explanation + .weaken_to_zero(bound) + .expect("not trivially satisfiable"); + + let HypercubeLinearExplanation::Proper(result_hl) = result else { + panic!("expected Proper variant"); + }; + + // x should be removed; only y with bound 2 remains. + let expected_linear = + LinearInequality::new([(NonZero::new(1).unwrap(), y)], 2).expect("not trivially sat"); + assert_eq!(result_hl.linear, expected_linear); + + // The bound predicate x >= 3 should have been added to the hypercube. + assert!( + result_hl + .hypercube + .iter_predicates() + .any(|p| p == predicate![x >= 3]) + ); + } + + #[test] + fn weaken_to_zero_proper_with_domain_absent_from_linear_leaves_linear_unchanged() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + let y = state.new_interval_variable(0, 10, Some("y".into())); + let z = state.new_interval_variable(0, 10, Some("z".into())); + + // y + z <= 5 — x is NOT in the linear + let linear = LinearInequality::new( + [(NonZero::new(1).unwrap(), y), (NonZero::new(1).unwrap(), z)], + 5, + ) + .expect("not trivially satisfiable"); + + let explanation = HypercubeLinearExplanation::Proper(HypercubeLinear { + hypercube: Hypercube::new([]).expect("not inconsistent"), + linear: linear.clone(), + }); + + // Weaken on x >= 3: x is not in the linear, so the linear should be unchanged. + let bound = BoundPredicate::new(predicate![x >= 3]).expect("bound predicate"); + let result = explanation + .weaken_to_zero(bound) + .expect("not trivially satisfiable"); + + let HypercubeLinearExplanation::Proper(result_hl) = result else { + panic!("expected Proper variant"); + }; + + assert_eq!(result_hl.linear, linear); + } +} diff --git a/pumpkin-crates/core/src/hypercube_linear/fake_trail.rs b/pumpkin-crates/core/src/hypercube_linear/fake_trail.rs new file mode 100644 index 000000000..0ed380864 --- /dev/null +++ b/pumpkin-crates/core/src/hypercube_linear/fake_trail.rs @@ -0,0 +1,252 @@ +use crate::containers::HashMap; +use crate::containers::KeyedVec; +use crate::hypercube_linear::explanation::HypercubeLinearExplanation; +use crate::hypercube_linear::trail_view::TrailView; +use crate::predicates::Predicate; +use crate::variables::DomainId; + +struct FakeAssignment { + predicate: Predicate, + /// 1-based; position 0 = "before all assignments" = initial state. + trail_position: usize, + checkpoint: usize, +} + +pub(super) struct FakeTrail { + current_checkpoint: usize, + assignments: Vec, + reasons: HashMap, + initial_bounds: KeyedVec, +} + +pub(super) struct FakeTrailBuilder { + current_builder_checkpoint: usize, + next_trail_position: usize, + assignments: Vec, + reasons: HashMap, + initial_bounds: KeyedVec, +} + +impl FakeTrailBuilder { + pub(super) fn domain(&mut self, lower: i32, upper: i32) -> DomainId { + self.initial_bounds.push((lower, upper)) + } + + pub(super) fn decide(mut self, predicate: Predicate) -> Self { + self.current_builder_checkpoint += 1; + self.push_predicate(predicate); + + self + } + + pub(super) fn propagate( + mut self, + predicate: Predicate, + reason: impl Into, + ) -> Self { + self.push_predicate(predicate); + let _ = self.reasons.insert(predicate, reason.into()); + self + } + + fn push_predicate(&mut self, predicate: Predicate) { + self.next_trail_position += 1; + let tp = self.next_trail_position; + self.assignments.push(FakeAssignment { + predicate, + trail_position: tp, + checkpoint: self.current_builder_checkpoint, + }); + } + + pub(super) fn build(self) -> FakeTrail { + FakeTrail { + current_checkpoint: self.current_builder_checkpoint, + assignments: self.assignments, + reasons: self.reasons, + initial_bounds: self.initial_bounds, + } + } +} + +impl FakeTrail { + pub(super) fn builder() -> FakeTrailBuilder { + FakeTrailBuilder { + current_builder_checkpoint: 0, + next_trail_position: 0, + assignments: vec![], + reasons: HashMap::default(), + initial_bounds: KeyedVec::default(), + } + } +} + +impl TrailView for FakeTrail { + fn trail_position(&self, predicate: Predicate) -> Option { + self.assignments + .iter() + .find(|a| a.predicate == predicate) + .map(|a| a.trail_position) + } + + fn checkpoint_for_predicate(&self, predicate: Predicate) -> Option { + let domain = predicate.get_domain(); + let rhs = predicate.get_right_hand_side(); + + let (initial_lb, initial_ub) = self + .initial_bounds + .get(domain) + .copied() + .unwrap_or((i32::MIN, i32::MAX)); + + // Check if initial bounds already satisfy the predicate. + if predicate_satisfied_by_bounds(predicate, initial_lb, initial_ub) { + return Some(0); + } + + // Scan assignments in trail order, tracking running lb/ub for the domain. + let mut lb = initial_lb; + let mut ub = initial_ub; + let mut domain_assignments: Vec<_> = self + .assignments + .iter() + .filter(|a| a.predicate.get_domain() == domain) + .collect(); + domain_assignments.sort_by_key(|a| a.trail_position); + + for assignment in domain_assignments { + let v = assignment.predicate.get_right_hand_side(); + if assignment.predicate.is_lower_bound_predicate() { + lb = lb.max(v); + } else if assignment.predicate.is_upper_bound_predicate() { + ub = ub.min(v); + } + let satisfied = if predicate.is_lower_bound_predicate() { + lb >= rhs + } else if predicate.is_upper_bound_predicate() { + ub <= rhs + } else { + false + }; + if satisfied { + return Some(assignment.checkpoint); + } + } + + None + } + + fn current_checkpoint(&self) -> usize { + self.current_checkpoint + } + + fn trail_position_at_checkpoint(&self, checkpoint: usize) -> usize { + // Return the last trail position of an assignment at this checkpoint, + // or 0 (initial state) if no assignments exist at this checkpoint. + self.assignments + .iter() + .filter(|a| a.checkpoint == checkpoint) + .map(|a| a.trail_position) + .max() + .unwrap_or(0) + } + + fn predicate_at_trail_position(&self, trail_position: usize) -> Predicate { + self.assignments + .iter() + .find(|a| a.trail_position == trail_position) + .map(|a| a.predicate) + .expect("no assignment at trail position") + } + + fn truth_value_at(&self, predicate: Predicate, trail_position: usize) -> Option { + let domain = predicate.get_domain(); + let rhs = predicate.get_right_hand_side(); + let lb = self.lower_bound_at_trail_position(domain, trail_position); + let ub = self.upper_bound_at_trail_position(domain, trail_position); + + if predicate.is_lower_bound_predicate() { + if lb >= rhs { + Some(true) + } else if ub < rhs { + Some(false) + } else { + None + } + } else if predicate.is_upper_bound_predicate() { + if ub <= rhs { + Some(true) + } else if lb > rhs { + Some(false) + } else { + None + } + } else { + None + } + } + + fn lower_bound_at_trail_position(&self, domain: DomainId, trail_position: usize) -> i32 { + let initial = self + .initial_bounds + .get(domain) + .map(|(lb, _)| *lb) + .unwrap_or(i32::MIN); + + self.assignments + .iter() + .filter(|a| { + a.trail_position <= trail_position + && a.predicate.get_domain() == domain + && a.predicate.is_lower_bound_predicate() + }) + .map(|a| a.predicate.get_right_hand_side()) + .fold(initial, i32::max) + } + + fn upper_bound_at_trail_position(&self, domain: DomainId, trail_position: usize) -> i32 { + let initial = self + .initial_bounds + .get(domain) + .map(|(_, ub)| *ub) + .unwrap_or(i32::MAX); + + self.assignments + .iter() + .filter(|a| { + a.trail_position <= trail_position + && a.predicate.get_domain() == domain + && a.predicate.is_upper_bound_predicate() + }) + .map(|a| a.predicate.get_right_hand_side()) + .fold(initial, i32::min) + } + + fn reason_for(&mut self, predicate: Predicate) -> HypercubeLinearExplanation { + self.reasons + .get(&predicate) + .cloned() + .unwrap_or_else(|| panic!("no reason stored for predicate {predicate}")) + } + + fn current_trail_position(&self) -> usize { + self.assignments + .iter() + .map(|a| a.trail_position) + .max() + .unwrap_or(0) + } +} + +fn predicate_satisfied_by_bounds(predicate: Predicate, initial_lb: i32, initial_ub: i32) -> bool { + use crate::predicates::PredicateType::*; + + let rhs = predicate.get_right_hand_side(); + + match predicate.get_predicate_type() { + LowerBound => rhs <= initial_lb, + NotEqual => rhs < initial_lb || rhs > initial_ub, + Equal => rhs == initial_lb && initial_lb == initial_ub, + UpperBound => rhs >= initial_ub, + } +} diff --git a/pumpkin-crates/core/src/hypercube_linear/hypercube.rs b/pumpkin-crates/core/src/hypercube_linear/hypercube.rs new file mode 100644 index 000000000..703843341 --- /dev/null +++ b/pumpkin-crates/core/src/hypercube_linear/hypercube.rs @@ -0,0 +1,612 @@ +use std::fmt::Display; +use std::hash::Hash; + +use itertools::Itertools; +use pumpkin_checking::IntExt; +use pumpkin_checking::VariableState; + +use crate::predicate; +use crate::predicates::Predicate; +use crate::predicates::PredicateType; +use crate::variables::DomainId; +use crate::variables::IntegerVariable; + +/// Error that occurs when constructing a [`Hypercube`]. +/// +/// If the domain of a variable becomes empty, the hypercube is inconsistent and cannot be +/// constructed. +#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)] +#[error("domain {0} is empty in the hypercube")] +pub struct InconsistentHypercube(DomainId); + +/// A region in the solution space. +/// +/// The hypercube will always be consistent. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Hypercube { + state: VariableState, + predicates: Vec, +} + +impl Hash for Hypercube { + fn hash(&self, hash_state: &mut H) { + for predicate in self.iter_predicates() { + predicate.hash(hash_state); + } + } +} + +impl Hypercube { + /// Creates a new [`Hypercube`] from a single predicate. + /// + /// Since a single predicate cannot be inconsistent, a hypercube can always be constructed. + pub fn from_single_predicate(predicate: Predicate) -> Hypercube { + Hypercube::new([predicate]).expect("single predicate cannot be inconsistent") + } + + /// Create a new hypercube from a sequence of predicates. + /// + /// If the predicates are inconsistent, the [`Err`] variant is returned. + pub fn new( + predicates: impl IntoIterator, + ) -> Result { + // Note: Ideally this would be an implementation of [`TryFrom`], however, that cannot be + // done in the same way due to a 'conflicting implementations' error. + + Hypercube::default().with_predicates(predicates) + } + + /// Add a predicate to the hypercube. + /// + /// If the new predicate causes the hypercube to be inconsistent, an error is returned. + pub fn with_predicate( + mut self, + predicate: Predicate, + ) -> Result { + if self.state.is_true(&predicate) { + // The predicate is subsumed by the existing predicates in the hypercube. + Ok(self) + } else if self.state.apply(&predicate) { + // We know that the new predicate is not implied by the hypercube, so it + // must be inserted. + let index_to_insert = self + .predicates + .binary_search(&predicate) + .expect_err("the predicate does not exist in the hypercube"); + + self.predicates.insert(index_to_insert, predicate); + minimize(&mut self.predicates); + + if cfg!(feature = "hl-checks") { + assert_eq!(self.predicates, describe_state_with_predicates(&self.state)); + } + + Ok(self) + } else { + Err(InconsistentHypercube(predicate.get_domain())) + } + } + + /// Add all predicates from the iterator to self. + pub fn with_predicates( + mut self, + predicates: impl IntoIterator, + ) -> Result { + let applied_predicates = predicates.into_iter().filter_map(|p| { + if self.state.is_true(&p) { + None + } else if self.state.apply(&p) { + Some(Ok(p)) + } else { + Some(Err(InconsistentHypercube(p.get_domain()))) + } + }); + + for maybe_predicate in applied_predicates { + let predicate = maybe_predicate?; + + let index_to_insert = self + .predicates + .binary_search(&predicate) + .expect_err("the predicate does not exist in the hypercube"); + + self.predicates.insert(index_to_insert, predicate); + } + + minimize(&mut self.predicates); + + if cfg!(feature = "hl-checks") { + assert_eq!(self.predicates, describe_state_with_predicates(&self.state)); + } + + Ok(self) + } + + /// Get all predicates that define the hypercube. + /// + /// Predicates are yielded in sorted order. + pub fn iter_predicates(&self) -> impl Iterator + '_ { + self.predicates.iter().copied() + } + + pub fn lower_bound(&self, term: &impl IntegerVariable) -> i32 { + match term.induced_lower_bound(&self.state) { + IntExt::Int(bound) => bound, + IntExt::NegativeInf => i32::MIN, + IntExt::PositiveInf => i32::MAX, + } + } +} + +/// Semantically minimize the given vector of predicates. +/// +/// If the conjunction of predicates is inconsistent, the behavior is unspecified. +/// +/// Panics if the vector is not sorted. +fn minimize(predicates: &mut Vec) { + assert!(predicates.is_sorted()); + + if predicates.len() < 2 { + // At least two elements are needed to require minimization. + return; + } + + // In the first pass, we tighten the lower bounds as much as possible. + let mut read_idx = 1; + let mut write_idx = 0; + + while read_idx < predicates.len() { + let p1 = predicates[write_idx]; + let p2 = predicates[read_idx]; + + match tighten_lower_bound(p1, p2) { + Some(p) => { + predicates[write_idx] = p; + read_idx += 1; + } + None => { + write_idx += 1; + predicates[write_idx] = predicates[read_idx]; + read_idx += 1; + } + } + } + + predicates.truncate(write_idx + 1); + + if predicates.len() < 2 { + // The truncate call may have reduced the length to 1. + return; + } + + // In a second pass, we tighten the upper bounds and identify equality. + let mut read_idx = 1; + let mut write_idx = 0; + + let num_predicates = predicates.len(); + let reverse_idx = |idx: usize| num_predicates - idx - 1; + + while read_idx < predicates.len() { + let p1 = predicates[reverse_idx(read_idx)]; + let p2 = predicates[reverse_idx(write_idx)]; + + match tighten_upper_bound_and_merge_equalities(p1, p2) { + Some(p) => { + predicates[reverse_idx(write_idx)] = p; + read_idx += 1; + } + None => { + write_idx += 1; + predicates[reverse_idx(write_idx)] = predicates[reverse_idx(read_idx)]; + read_idx += 1; + } + } + } + + let end_of_range_to_drain = predicates.len() - write_idx - 1; + + // Shift everything to the right. + let _ = predicates.drain(..end_of_range_to_drain); +} + +fn tighten_upper_bound_and_merge_equalities(p1: Predicate, p2: Predicate) -> Option { + use PredicateType::*; + assert!(p1 <= p2); + + if p1.get_domain() != p2.get_domain() { + return None; + } + + let domain = p1.get_domain(); + let p1_rhs = p1.get_right_hand_side(); + let p2_rhs = p2.get_right_hand_side(); + + match (p1.get_predicate_type(), p2.get_predicate_type()) { + (NotEqual, UpperBound) => { + if p1_rhs == p2_rhs { + return Some(predicate![domain <= p1_rhs - 1]); + } + if p1_rhs > p2_rhs { + return Some(p2); + } + } + (UpperBound, UpperBound) => return Some(p1), + + (Equal, UpperBound) => return Some(p1), + + (LowerBound, Equal) => return Some(p2), + (NotEqual, Equal) => return Some(p2), + + (LowerBound, UpperBound) => { + if p1_rhs == p2_rhs { + return Some(predicate![domain == p1_rhs]); + } + } + + // Handled in forward pass. + (LowerBound, LowerBound) + | (LowerBound, NotEqual) + | (Equal, NotEqual) + | (NotEqual, NotEqual) => {} + + (UpperBound, LowerBound) + | (UpperBound, NotEqual) + | (UpperBound, Equal) + | (NotEqual, LowerBound) + | (Equal, LowerBound) + | (Equal, Equal) => unreachable!("predicate order: p1 = {p1}, p2 = {p2}"), + } + + None +} + +fn tighten_lower_bound(p1: Predicate, p2: Predicate) -> Option { + use PredicateType::*; + + assert!(p1 <= p2); + + if p1.get_domain() != p2.get_domain() { + return None; + } + + let domain = p1.get_domain(); + let p1_rhs = p1.get_right_hand_side(); + let p2_rhs = p2.get_right_hand_side(); + + match (p1.get_predicate_type(), p2.get_predicate_type()) { + (LowerBound, LowerBound) => { + return Some(p2); + } + (LowerBound, NotEqual) => { + if p1_rhs > p2_rhs { + // E.g. [x >= 5] & [x != 4] -> [x >= 5] + return Some(p1); + } + + if p1_rhs == p2_rhs { + // E.g. [x >= 5] & [x != 5] -> [x >= 6] + return Some(predicate![domain >= p1_rhs + 1]); + } + } + (LowerBound, UpperBound) => { + if p1_rhs == p2_rhs { + return Some(predicate![domain == p1_rhs]); + } + } + (LowerBound, Equal) => { + assert!(p1_rhs <= p2_rhs); + return Some(p2); + } + + (NotEqual, Equal) => return Some(p2), + (Equal, NotEqual) => return Some(p1), + + // Handled in backward pass. + (NotEqual, UpperBound) + | (UpperBound, UpperBound) + | (Equal, UpperBound) + | (NotEqual, NotEqual) => {} + + (UpperBound, LowerBound) + | (UpperBound, NotEqual) + | (UpperBound, Equal) + | (NotEqual, LowerBound) + | (Equal, LowerBound) + | (Equal, Equal) => unreachable!("predicate order: p1 = {p1}, p2 = {p2}"), + } + + None +} + +impl Display for Hypercube { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.iter_predicates().format(" & ")) + } +} + +/// Get a vector of predicates that describe the given state. +fn describe_state_with_predicates(state: &VariableState) -> Vec { + let mut predicates = state + .domains() + .flat_map(|domain_id| { + if let Some(value) = state.fixed_value(domain_id) { + itertools::Either::Left(std::iter::once(predicate![domain_id == value])) + } else { + let lower_bound_predicate = + if let IntExt::Int(lower_bound) = state.lower_bound(domain_id) { + Some(predicate![domain_id >= lower_bound]) + } else { + None + }; + let upper_bound_predicate = + if let IntExt::Int(upper_bound) = state.upper_bound(domain_id) { + Some(predicate![domain_id <= upper_bound]) + } else { + None + }; + + itertools::Either::Right( + lower_bound_predicate + .into_iter() + .chain( + state + .holes(domain_id) + .map(|value| predicate![domain_id != value]), + ) + .chain(upper_bound_predicate), + ) + } + }) + .collect::>(); + + predicates.sort(); + + predicates +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::conjunction; + use crate::containers::HashSet; + use crate::predicate; + use crate::state::State; + + #[test] + fn consistent_hypercube_can_be_created() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 10, Some("x".into())); + let y = state.new_interval_variable(1, 10, Some("y".into())); + + let maybe_hypercube = Hypercube::new(conjunction!([x >= 2] & [y >= 2])); + + assert!(maybe_hypercube.is_ok()); + } + + #[test] + fn inconsistent_hypercube_can_be_created() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 10, Some("x".into())); + let y = state.new_interval_variable(1, 10, Some("y".into())); + + let error = Hypercube::new(conjunction!([x >= 2] & [y >= 2] & [x <= 1])) + .expect_err("hypercube is inconsistent"); + + assert_eq!(InconsistentHypercube(x), error); + } + + #[test] + fn hypercube_iters_predicates_from_constructor() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 10, Some("x".into())); + let y = state.new_interval_variable(1, 10, Some("y".into())); + + let hypercube = + Hypercube::new(conjunction!([x >= 2] & [y >= 2])).expect("not inconsistent"); + + assert_eq!( + [predicate![x >= 2], predicate![y >= 2]] + .into_iter() + .collect::>(), + hypercube.iter_predicates().collect::>(), + ); + } + + #[test] + fn iterating_predicates_ignores_subsumed_predicates() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 10, Some("x".into())); + + let hypercube = + Hypercube::new(conjunction!([x >= 2] & [x >= 4])).expect("not inconsistent"); + + assert_eq!( + [predicate![x >= 4]].into_iter().collect::>(), + hypercube.iter_predicates().collect::>(), + ); + } + + #[test] + fn iterated_predicates_are_returned_in_sorted_order() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 10, Some("x".into())); + let y = state.new_interval_variable(1, 10, Some("y".into())); + + let hypercube = + Hypercube::new(conjunction!([y >= 2] & [x <= 6] & [x >= 4])).expect("not inconsistent"); + + assert_eq!( + vec![predicate![x >= 4], predicate![x <= 6], predicate![y >= 2]], + hypercube.iter_predicates().collect::>(), + ); + } + + #[test] + fn strengthening_the_hypercube_does_not_introduce_duplicate_predicates() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 10, Some("x".into())); + + let hypercube = Hypercube::from_single_predicate(predicate![x >= 4]) + .with_predicate(predicate![x >= 6]) + .expect("not inconsistent"); + + assert_eq!( + vec![predicate![x >= 6]], + hypercube.iter_predicates().collect::>(), + ); + } + + #[test] + fn adding_equality_removes_everything_else() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 10, Some("x".into())); + + let hypercube = Hypercube::new(conjunction!([x >= 4] & [x != 5] & [x <= 7])) + .expect("not inconsistent") + .with_predicate(predicate![x == 6]) + .expect("not inconsistent"); + + assert_eq!( + vec![predicate![x == 6]], + hypercube.iter_predicates().collect::>(), + ); + } + + #[test] + fn lower_bound_and_not_equal_imply_tighter_lower_bound() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 10, Some("x".into())); + + let hypercube = Hypercube::from_single_predicate(predicate![x >= 4]) + .with_predicate(predicate![x != 4]) + .expect("not inconsistent"); + + assert_eq!( + vec![predicate![x >= 5]], + hypercube.iter_predicates().collect::>(), + ); + } + + #[test] + fn upper_bound_and_not_equal_imply_tighter_upper_bound() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 10, Some("x".into())); + + let hypercube = Hypercube::from_single_predicate(predicate![x <= 4]) + .with_predicate(predicate![x != 4]) + .expect("not inconsistent"); + + assert_eq!( + vec![predicate![x <= 3]], + hypercube.iter_predicates().collect::>(), + ); + } + + #[test] + fn new_hole_may_tighten_bound_a_lot() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 10, Some("x".into())); + + let hypercube = Hypercube::new(conjunction!([x >= 4] & [x != 5])) + .expect("not inconsistent") + .with_predicate(predicate![x != 4]) + .expect("not inconsistent"); + + assert_eq!( + vec![predicate![x >= 6]], + hypercube.iter_predicates().collect::>(), + ); + } + + #[test] + fn new_hole_can_imply_equality() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 10, Some("x".into())); + + let hypercube = Hypercube::new(conjunction!([x >= 4] & [x <= 5])) + .expect("not inconsistent") + .with_predicate(predicate![x != 4]) + .expect("not inconsistent"); + + assert_eq!( + vec![predicate![x == 5]], + hypercube.iter_predicates().collect::>(), + ); + } + + #[test] + fn different_domains_are_not_combined_during_minimization() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 10, Some("x".into())); + let y = state.new_interval_variable(1, 10, Some("y".into())); + + let hypercube = + Hypercube::new(conjunction!([x == 10] & [y == 10])).expect("not inconsistent"); + + assert_eq!( + vec![predicate![x == 10], predicate![y == 10]], + hypercube.iter_predicates().collect::>(), + ); + } + + #[test] + fn lower_and_upper_bound_are_merged_if_hole_tightens_upper() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 10, Some("x".into())); + + let hypercube = Hypercube::new(conjunction!([x >= 9] & [x != 10])) + .expect("not inconsistent") + .with_predicate(predicate![x <= 10]) + .expect("not inconsistent"); + + assert_eq!( + vec![predicate![x == 9]], + hypercube.iter_predicates().collect::>(), + ); + } + + #[test] + fn upper_bound_is_tightened() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 10, Some("x".into())); + + let hypercube = Hypercube::from_single_predicate(predicate![x <= 0]) + .with_predicate(predicate![x <= -2]) + .expect("not inconsistent"); + + assert_eq!( + vec![predicate![x <= -2]], + hypercube.iter_predicates().collect::>(), + ); + } + + #[test] + fn not_equal_followed_by_equal_is_removed() { + let mut state = State::default(); + + let x = state.new_interval_variable(1, 10, Some("x".into())); + + let hypercube = + Hypercube::new([predicate![x != 1], predicate![x != 2], predicate![x == 4]]) + .expect("not inconsistent"); + + assert_eq!( + vec![predicate![x == 4]], + hypercube.iter_predicates().collect::>(), + ); + } +} diff --git a/pumpkin-crates/core/src/propagators/hypercube_linear/linear.rs b/pumpkin-crates/core/src/hypercube_linear/linear.rs similarity index 52% rename from pumpkin-crates/core/src/propagators/hypercube_linear/linear.rs rename to pumpkin-crates/core/src/hypercube_linear/linear.rs index 94704c363..6003f4417 100644 --- a/pumpkin-crates/core/src/propagators/hypercube_linear/linear.rs +++ b/pumpkin-crates/core/src/hypercube_linear/linear.rs @@ -1,17 +1,29 @@ +use std::fmt::Display; use std::num::NonZero; +use itertools::Itertools; + use crate::containers::HashMap; +use crate::hypercube_linear::BoundComparator; +use crate::hypercube_linear::BoundPredicate; +use crate::math::num_ext::NumExt; use crate::variables::AffineView; use crate::variables::DomainId; use crate::variables::TransformableVariable; /// The linear inequality part of a hypercube linear constraint. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct LinearInequality { - terms: Box<[AffineView]>, + terms: Vec>, bound: i32, } +impl Default for LinearInequality { + fn default() -> Self { + LinearInequality::trivially_false() + } +} + impl LinearInequality { /// Create a linear inequality that is trivially false. pub fn trivially_false() -> LinearInequality { @@ -37,16 +49,18 @@ impl LinearInequality { *existing_weight += weight.get(); } - let terms = domain_to_weight + let mut terms = domain_to_weight .into_iter() .filter(|&(_, weight)| weight != 0) .map(|(domain, weight)| domain.scaled(weight)) - .collect::>(); + .collect::>(); if terms.is_empty() && bound >= 0 { return None; } + terms.sort_by_key(|t| t.inner); + Some(LinearInequality { terms, bound }) } @@ -69,6 +83,103 @@ impl LinearInequality { pub fn term_for_domain(&self, domain: DomainId) -> Option> { self.terms().find(|view| view.inner == domain) } + + /// Divide the linear inequality and round the bound down. + /// + /// The divisor _must_ divide all term weights, otherwise this function panics. + pub fn divide(&mut self, divisor: i32) { + for term in self.terms.iter_mut() { + assert_eq!(term.scale % divisor, 0); + term.scale /= divisor; + } + + self.bound = ::div_floor(self.bound, divisor); + } + + /// Weakens the linear inequality on the given bound. + /// + /// Does nothing if the bound does not contribute to the slack of the linear. + pub fn weaken(mut self, bound: BoundPredicate, count: i32) -> Option { + let Some(term_idx) = self + .terms + .iter() + .position(|term| term.inner == bound.domain) + else { + return Some(self); + }; + + let term = &mut self.terms[term_idx]; + let contributes_to_slack = (term.scale.is_positive() + && bound.comparator == BoundComparator::LowerBound) + || (term.scale.is_negative() && bound.comparator == BoundComparator::UpperBound); + + if !contributes_to_slack { + return Some(self); + } + + let signed_diff = match bound.comparator { + BoundComparator::LowerBound => -count, + BoundComparator::UpperBound => count, + }; + + term.scale += signed_diff; + self.bound += signed_diff * bound.value; + + if term.scale == 0 { + let _ = self.terms.remove(term_idx); + } + + if self.terms.is_empty() && self.bound >= 0 { + None + } else { + Some(self) + } + } + + /// Weakens the linear inequality on the given bound and ensures the weight of the domain + /// of the bound is 0. + /// + /// Does nothing if the bound does not contribute to the slack of the linear. + pub fn weaken_to_zero(self, bound: BoundPredicate) -> Option { + let Some(term) = self.term_for_domain(bound.domain) else { + return Some(self); + }; + + self.weaken(bound, term.scale.abs()) + } +} + +impl Display for LinearInequality { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} <= {}", + self.terms().format_with(" ", |elt, f| f(&format_args!( + "{} {}", + elt.scale, elt.inner + ))), + self.bound(), + ) + } +} + +/// A convenient helper to construct linear inequalities. +/// +/// ## Examples +/// In the following, variable bindings are [`DomainId`]s. +/// ```ignore +/// linear_inequality(2 x + 3 y <= 4); +/// linear_inequality(-1 x + 5 y <= -3); +/// ``` +#[macro_export] +macro_rules! linear_inequality { + ($($weight:literal $var:ident $(+)?)* <= $bound:expr) => {{ + let terms = [$(( + std::num::NonZero::new($weight).unwrap(), + $var, + ),)*]; + LinearInequality::new(terms, $bound).unwrap() + }}; } #[cfg(test)] @@ -157,4 +268,22 @@ mod tests { linear.terms().collect::>() ); } + + #[test] + fn macro_handles_linears() { + let x = DomainId::new(0); + let y = DomainId::new(1); + + let actual = linear_inequality!(2 x + -3 y <= 5); + let expected = LinearInequality::new( + [ + (NonZero::new(2).unwrap(), x), + (NonZero::new(-3).unwrap(), y), + ], + 5, + ) + .expect("not trivially satisfiable"); + + assert_eq!(actual, expected); + } } diff --git a/pumpkin-crates/core/src/hypercube_linear/mod.rs b/pumpkin-crates/core/src/hypercube_linear/mod.rs new file mode 100644 index 000000000..6c6344ef5 --- /dev/null +++ b/pumpkin-crates/core/src/hypercube_linear/mod.rs @@ -0,0 +1,26 @@ +mod bound_predicate; +mod checker; +mod conflict_state; +mod constraint; +mod explanation; +mod hypercube; +mod linear; +mod predicate_heap; +mod propagator; +mod resh_strategy; +mod resolver; +mod trace; +mod trail_view; + +#[cfg(test)] +mod fake_trail; + +pub use bound_predicate::*; +pub use checker::*; +pub use constraint::*; +pub use explanation::*; +pub use hypercube::*; +pub use linear::*; +pub use propagator::*; +pub use resolver::*; +pub use trace::*; diff --git a/pumpkin-crates/core/src/hypercube_linear/predicate_heap.rs b/pumpkin-crates/core/src/hypercube_linear/predicate_heap.rs new file mode 100644 index 000000000..62a8c5d9c --- /dev/null +++ b/pumpkin-crates/core/src/hypercube_linear/predicate_heap.rs @@ -0,0 +1,470 @@ +use std::cmp::Ordering; +use std::collections::BinaryHeap; + +use crate::hypercube_linear::trail_view::TrailView; +use crate::predicates::Predicate; +use crate::predicates::PredicateType; + +/// A max-heap of predicates. The keys are based on the trail positions of the predicates in the +/// state, meaning predicates are popped in reverse trail order. Implied predicates are popped +/// before the predicate on the trail that implies the predicate. +#[derive(Clone, Debug, Default)] +pub(crate) struct PredicateHeap { + heap: BinaryHeap, +} + +impl PredicateHeap { + /// See [`BinaryHeap::is_empty`]. + pub(crate) fn is_empty(&mut self) -> bool { + self.heap.is_empty() + } + + /// See [`BinaryHeap::pop`]. + /// + /// Predicates are popped in reverse-trail order, and implied predicates are ordered such + /// that stronger predicates go before weaker predicates. + pub(crate) fn pop(&mut self) -> Option { + self.heap.pop().map(|to_explain| to_explain.predicate) + } + + /// See [`BinaryHeap::drain`]. + pub(crate) fn drain(&mut self) -> impl ExactSizeIterator + '_ { + self.heap.drain().map(|to_explain| to_explain.predicate) + } + + /// See [`BinaryHeap::retain`]. + pub(crate) fn retain(&mut self, mut f: impl FnMut(Predicate) -> bool) { + self.heap.retain(move |pte| f(pte.predicate)); + } + + /// See [`BinaryHeap::iter`]. + pub(crate) fn iter(&self) -> impl ExactSizeIterator + '_ { + self.heap.iter().map(|to_explain| to_explain.predicate) + } + + /// Push a new predicate onto the heap. + /// + /// If the predicate is not true in the given trail, this method panics. + pub(crate) fn push(&mut self, predicate: Predicate, trail: &T) { + // TODO: This can probably be optimized. But only do so once profiling shows this + // as a problem. + if self.heap.iter().any(|pte| pte.predicate == predicate) { + return; + } + + let trail_position = trail + .trail_position(predicate) + .expect("predicate must be true in given trail"); + + let is_implied = trail.predicate_at_trail_position(trail_position) != predicate; + + self.heap.push(PredicateToExplain { + predicate, + trail_position, + is_implied, + }); + + if cfg!(feature = "hl-checks") { + assert_eq!( + self.heap + .iter() + .filter(|pte| pte.predicate == predicate) + .count(), + 1 + ); + } + } + + /// Returns true if the given [`Predicate`] is part of the heap. + pub(crate) fn contains(&self, predicate: Predicate) -> bool { + self.heap.iter().any(|pte| pte.predicate == predicate) + } +} + +/// Used to order the predicates in the [`PredicateHeap`]. +/// +/// The priority is calculated based on the trail position of the predicate and whether the +/// predicate is on the trail or implied. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct PredicateToExplain { + predicate: Predicate, + trail_position: usize, + is_implied: bool, +} + +impl PartialOrd for PredicateToExplain { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PredicateToExplain { + /// Returns a "virtual rank" that approximates where this predicate would appear in an + /// imagined SAT encoding of the CP trail. Higher rank = pops first from the max-heap. + /// + /// When two predicates share the same CP trail position, we order them as if the CP solver + /// were a SAT solver that lazily propagates implied predicates after the direct trail entry: + /// + /// - **Rank 4** – `NotEqual` implied by the trail entry (a propagated consequence of an + /// equality or a bound that excluded the value): these are the "furthest derived" facts. + /// - **Rank 3** – `Equal` implied by a combination of bounds: the equality was established only + /// once both bounds met, making it more derived than either individual bound. + /// - **Rank 2** – The direct trail entry itself (`Equal`, `LowerBound`, or `UpperBound`): the + /// actual CP fact that was recorded. + /// - **Rank 1** – Implied `LowerBound`/`UpperBound`: weaker bounds that became true as a + /// side-effect of the direct entry but carry strictly less information. + /// - **Rank 0** – Direct `NotEqual` trail entry: these are typically narrow, single-value + /// removals whose implied equalities and bounds are of higher semantic content. + fn virtual_rank(&self) -> u8 { + match (self.predicate.get_predicate_type(), self.is_implied) { + (PredicateType::NotEqual, true) => 4, + (PredicateType::Equal, true) => 3, + ( + PredicateType::Equal | PredicateType::LowerBound | PredicateType::UpperBound, + false, + ) => 2, + (PredicateType::LowerBound | PredicateType::UpperBound, true) => 1, + (PredicateType::NotEqual, false) => 0, + } + } +} + +impl Ord for PredicateToExplain { + fn cmp(&self, other: &Self) -> Ordering { + let trail_order = self.trail_position.cmp(&other.trail_position); + + if matches!(trail_order, Ordering::Less | Ordering::Greater) { + // If the predicates are from different trail positions, then the order of + // the trail position is the order of the predicates in the heap. + return trail_order; + } + + assert_eq!(trail_order, Ordering::Equal); + assert_eq!(self.predicate.get_domain(), other.predicate.get_domain()); + + let self_rhs = self.predicate.get_right_hand_side(); + let other_rhs = other.predicate.get_right_hand_side(); + + // For same-type bounds, compare by value strength directly. A bound implied by + // the trail entry may be *stronger* than the direct entry (e.g. when x != k + // was already known, posting x >= k propagates to x >= k+1). Virtual rank + // cannot capture this because it treats all implied LB/UB uniformly. + match ( + self.predicate.get_predicate_type(), + other.predicate.get_predicate_type(), + ) { + // Stronger lower bound (higher value) gets higher priority. + (PredicateType::LowerBound, PredicateType::LowerBound) => { + return self_rhs.cmp(&other_rhs); + } + // Stronger upper bound (lower value) gets higher priority. + (PredicateType::UpperBound, PredicateType::UpperBound) => { + return other_rhs.cmp(&self_rhs); + } + _ => {} + } + + match self.virtual_rank().cmp(&other.virtual_rank()) { + ord @ (Ordering::Less | Ordering::Greater) => ord, + + // Same virtual rank: break ties by predicate strength / value. + Ordering::Equal => { + match ( + self.predicate.get_predicate_type(), + other.predicate.get_predicate_type(), + ) { + // Among implied not-equals the relative order is unspecified; + // break ties by ascending RHS value for a deterministic result. + (PredicateType::NotEqual, PredicateType::NotEqual) => other_rhs.cmp(&self_rhs), + // Mixed types at the same rank are not expected but handled + // consistently via the natural predicate ordering. + _ => self.predicate.cmp(&other.predicate), + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::predicate; + use crate::state::State; + + fn post(state: &mut State, predicate: Predicate) { + let _ = state.post(predicate).unwrap(); + } + + #[test] + fn new_heap_is_empty() { + let mut heap = PredicateHeap::default(); + assert!(heap.is_empty()); + } + + #[test] + fn pop_from_empty_heap_returns_none() { + let mut heap = PredicateHeap::default(); + assert_eq!(heap.pop(), None); + } + + #[test] + fn is_not_empty_after_push() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + post(&mut state, predicate![x >= 3]); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x >= 3], &state); + + assert!(!heap.is_empty()); + } + + #[test] + fn push_and_pop_single_predicate() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + post(&mut state, predicate![x >= 3]); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x >= 3], &state); + + assert_eq!(heap.pop(), Some(predicate![x >= 3])); + assert!(heap.is_empty()); + } + + #[test] + fn predicates_are_popped_in_reverse_trail_order() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + let y = state.new_interval_variable(0, 10, Some("y".into())); + + // Post x >= 3 first, y >= 5 second — y has the higher trail position. + post(&mut state, predicate![x >= 3]); + post(&mut state, predicate![y >= 5]); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x >= 3], &state); + heap.push(predicate![y >= 5], &state); + + // y >= 5 was posted later (higher trail position) and is popped first. + assert_eq!(heap.pop(), Some(predicate![y >= 5])); + assert_eq!(heap.pop(), Some(predicate![x >= 3])); + assert!(heap.is_empty()); + } + + #[test] + fn duplicate_push_is_ignored() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + post(&mut state, predicate![x >= 3]); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x >= 3], &state); + heap.push(predicate![x >= 3], &state); + + assert_eq!(heap.pop(), Some(predicate![x >= 3])); + assert_eq!(heap.pop(), None); + } + + #[test] + fn contains_returns_true_for_pushed_predicate() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + post(&mut state, predicate![x >= 3]); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x >= 3], &state); + + assert!(heap.contains(predicate![x >= 3])); + } + + #[test] + fn contains_returns_false_for_absent_predicate() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + post(&mut state, predicate![x >= 5]); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x >= 5], &state); + + // x >= 4 is true in the state but was never pushed. + assert!(!heap.contains(predicate![x >= 4])); + } + + #[test] + fn retain_keeps_matching_predicates() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + let y = state.new_interval_variable(0, 10, Some("y".into())); + post(&mut state, predicate![x >= 3]); + post(&mut state, predicate![y >= 5]); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x >= 3], &state); + heap.push(predicate![y >= 5], &state); + + let y_domain = predicate![y >= 5].get_domain(); + heap.retain(|p| p.get_domain() == y_domain); + + assert!(!heap.contains(predicate![x >= 3])); + assert!(heap.contains(predicate![y >= 5])); + assert_eq!(heap.pop(), Some(predicate![y >= 5])); + assert!(heap.is_empty()); + } + + #[test] + fn drain_empties_the_heap_and_returns_all_elements() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + let y = state.new_interval_variable(0, 10, Some("y".into())); + post(&mut state, predicate![x >= 3]); + post(&mut state, predicate![y >= 5]); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x >= 3], &state); + heap.push(predicate![y >= 5], &state); + + let drained: Vec<_> = heap.drain().collect(); + + assert_eq!(drained.len(), 2); + assert!(drained.contains(&predicate![x >= 3])); + assert!(drained.contains(&predicate![y >= 5])); + assert!(heap.is_empty()); + } + + #[test] + fn iter_yields_all_pushed_predicates_without_removing_them() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + let y = state.new_interval_variable(0, 10, Some("y".into())); + post(&mut state, predicate![x >= 3]); + post(&mut state, predicate![y >= 5]); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x >= 3], &state); + heap.push(predicate![y >= 5], &state); + + let iterated: Vec<_> = heap.iter().collect(); + + assert_eq!(iterated.len(), 2); + assert!(iterated.contains(&predicate![x >= 3])); + assert!(iterated.contains(&predicate![y >= 5])); + // Predicates are still in the heap after iterating. + assert!(!heap.is_empty()); + } + + #[test] + fn implied_predicates_ordered_as_if_they_are_set_by_a_propagator() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + post(&mut state, predicate![x >= 5]); + + // Confirm both land at the same trail position. + assert_eq!( + state.trail_position(predicate![x >= 5]), + state.trail_position(predicate![x >= 4]), + ); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x >= 4], &state); // implied + heap.push(predicate![x >= 5], &state); // direct trail predicate + + assert_eq!(heap.pop(), Some(predicate![x >= 5])); + assert_eq!(heap.pop(), Some(predicate![x >= 4])); + assert_eq!(heap.pop(), None); + } + + #[test] + fn equality_is_implicitly_propagated_by_two_bounds() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + post(&mut state, predicate![x <= 5]); + post(&mut state, predicate![x >= 5]); + + assert_eq!(state.truth_value(predicate![x == 5]), Some(true)); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x == 5], &state); + heap.push(predicate![x >= 5], &state); + + assert_eq!(heap.pop(), Some(predicate![x == 5])); + assert_eq!(heap.pop(), Some(predicate![x >= 5])); + assert_eq!(heap.pop(), None); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x >= 5], &state); + heap.push(predicate![x == 5], &state); + + assert_eq!(heap.pop(), Some(predicate![x == 5])); + assert_eq!(heap.pop(), Some(predicate![x >= 5])); + assert_eq!(heap.pop(), None); + } + + #[test] + fn not_equals_are_implied_by_equality() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + post(&mut state, predicate![x == 5]); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x != 4], &state); + heap.push(predicate![x == 5], &state); + heap.push(predicate![x != 6], &state); + + assert_eq!(heap.pop(), Some(predicate![x != 4])); + assert_eq!(heap.pop(), Some(predicate![x != 6])); + assert_eq!(heap.pop(), Some(predicate![x == 5])); + assert_eq!(heap.pop(), None); + } + + #[test] + fn not_equals_may_imply_equality_if_bounds_are_present() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + post(&mut state, predicate![x >= 4]); + post(&mut state, predicate![x <= 5]); + post(&mut state, predicate![x != 4]); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x == 5], &state); + heap.push(predicate![x >= 5], &state); + heap.push(predicate![x != 4], &state); + + assert_eq!(heap.pop(), Some(predicate![x == 5])); + assert_eq!(heap.pop(), Some(predicate![x >= 5])); + assert_eq!(heap.pop(), Some(predicate![x != 4])); + assert_eq!(heap.pop(), None); + } + + #[test] + fn not_equals_with_lower_bound_implies_stronger_bound() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + post(&mut state, predicate![x != 4]); + post(&mut state, predicate![x >= 4]); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x >= 4], &state); + heap.push(predicate![x >= 5], &state); + + assert_eq!(heap.pop(), Some(predicate![x >= 5])); + assert_eq!(heap.pop(), Some(predicate![x >= 4])); + assert_eq!(heap.pop(), None); + } + + #[test] + fn not_equals_with_upper_bound_implies_stronger_bound() { + let mut state = State::default(); + let x = state.new_interval_variable(0, 10, Some("x".into())); + post(&mut state, predicate![x != 4]); + post(&mut state, predicate![x <= 4]); + + let mut heap = PredicateHeap::default(); + heap.push(predicate![x <= 4], &state); + heap.push(predicate![x <= 3], &state); + + assert_eq!(heap.pop(), Some(predicate![x <= 3])); + assert_eq!(heap.pop(), Some(predicate![x <= 4])); + assert_eq!(heap.pop(), None); + } +} diff --git a/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs b/pumpkin-crates/core/src/hypercube_linear/propagator.rs similarity index 79% rename from pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs rename to pumpkin-crates/core/src/hypercube_linear/propagator.rs index 1af0ef3d0..96dd381b6 100644 --- a/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs +++ b/pumpkin-crates/core/src/hypercube_linear/propagator.rs @@ -1,6 +1,11 @@ +use std::cmp::Reverse; + use crate::basic_types::PredicateId; use crate::declare_inference_label; use crate::engine::PropagationStatusCP; +use crate::hypercube_linear::Hypercube; +use crate::hypercube_linear::HypercubeLinearChecker; +use crate::hypercube_linear::LinearInequality; use crate::predicate; use crate::predicates::Predicate; use crate::predicates::PropositionalConjunction; @@ -14,9 +19,6 @@ use crate::propagation::Propagator; use crate::propagation::PropagatorConstructor; use crate::propagation::PropagatorConstructorContext; use crate::propagation::ReadDomains; -use crate::propagators::hypercube_linear::Hypercube; -use crate::propagators::hypercube_linear::HypercubeLinearChecker; -use crate::propagators::hypercube_linear::LinearInequality; use crate::pumpkin_assert_simple; use crate::state::PropagatorConflict; use crate::variables::AffineView; @@ -51,25 +53,43 @@ impl PropagatorConstructor for HypercubeLinearConstructor { constraint_tag, } = self; - let hypercube_predicates = hypercube.iter_predicates().collect::>(); + let mut hypercube_predicates = hypercube.iter_predicates().collect::>(); + + // Make sure the predicates with highest decision level are at the start. If + // predicates are not assigned, we consider them at the highest decision level. + hypercube_predicates.sort_unstable_by_key(|&p| { + Reverse( + context + .domains() + .get_checkpoint_for_predicate(p) + .unwrap_or(usize::MAX), + ) + }); - let watched_predicates = if hypercube_predicates.is_empty() { - let true_predicate = Predicate::trivially_true(); - let true_predicate_id = context.register_predicate(true_predicate); - [true_predicate_id; NUM_WATCHED_PREDICATES] - } else { - let last_idx = hypercube_predicates.len() - 1; - [ - context.register_predicate(hypercube_predicates[0]), - context.register_predicate(hypercube_predicates[1.min(last_idx)]), - ] - }; + #[allow(clippy::get_first, reason = "is more consistent")] + let watched_predicates = [ + context.register_predicate( + hypercube_predicates + .get(0) + .copied() + .unwrap_or_else(Predicate::trivially_true), + ), + context.register_predicate( + hypercube_predicates + .get(1) + .copied() + .unwrap_or_else(Predicate::trivially_true), + ), + ]; HypercubeLinearPropagator { + hypercube, linear, hypercube_predicates, watched_predicates, + is_watching_linear: false, + inference_code: InferenceCode::new(constraint_tag, HypercubeLinear), } } @@ -82,6 +102,7 @@ const NUM_WATCHED_PREDICATES: usize = 2; /// A [`Propagator`] for the hypercube linear constraint. #[derive(Clone, Debug)] pub struct HypercubeLinearPropagator { + hypercube: Hypercube, linear: LinearInequality, hypercube_predicates: Box<[Predicate]>, @@ -89,6 +110,9 @@ pub struct HypercubeLinearPropagator { /// `hypercube_predicates`. watched_predicates: [PredicateId; NUM_WATCHED_PREDICATES], + /// True when we are watching the linear inequality. + is_watching_linear: bool, + inference_code: InferenceCode, } @@ -142,19 +166,7 @@ impl HypercubeLinearPropagator { Err(_) => return Ok(()), }; - let reason = self - .linear - .terms() - .filter(|&t| t != term) - .map(|term| predicate![term >= context.lower_bound(&term)]) - .chain(self.hypercube_predicates.iter().copied()) - .collect::(); - - context.post( - predicate![term <= term_upper_bound], - reason, - &self.inference_code, - )?; + context.post(predicate![term <= term_upper_bound], 0_u64)?; } Ok(()) @@ -244,17 +256,36 @@ impl Propagator for HypercubeLinearPropagator { "HypercubeLinear" } + fn explain_as_hypercube_linear( + &mut self, + _code: u64, + _context: crate::propagation::ExplanationContext, + ) -> Option<(Hypercube, LinearInequality, InferenceCode)> { + Some(( + self.hypercube.clone(), + self.linear.clone(), + self.inference_code.clone(), + )) + } + fn propagate(&mut self, mut context: PropagationContext) -> PropagationStatusCP { let satisfied_watchers = self.update_watched_predicates(context.reborrow()); if satisfied_watchers < NUM_WATCHED_PREDICATES - 1 { - self.unregister_bound_events_on_linear(context.reborrow()); + if self.is_watching_linear { + self.unregister_bound_events_on_linear(context.reborrow()); + self.is_watching_linear = false; + } + // More than one watcher is unassigned, so we do not need to propagate anything. return Ok(()); - } else if satisfied_watchers == NUM_WATCHED_PREDICATES { + } else { // The hypercube is satisfied, so we should be registered to bound events on the terms // of the linear inequality. - self.register_bound_events_on_linear(context.reborrow()); + if !self.is_watching_linear { + self.register_bound_events_on_linear(context.reborrow()); + self.is_watching_linear = true; + } } let unassigned_watcher_index = self.unassigned_watcher_index(context.reborrow()); @@ -262,7 +293,12 @@ impl Propagator for HypercubeLinearPropagator { let lower_bound_terms = self .linear .terms() - .map(|term| i64::from(context.lower_bound(&term))) + .map(|term| { + let bound_in_state = context.lower_bound(&term); + let bound_in_hypercube = self.hypercube.lower_bound(&term); + + i64::from(i32::max(bound_in_state, bound_in_hypercube)) + }) .sum::(); let slack = i64::from(self.linear.bound()) - lower_bound_terms; @@ -280,19 +316,7 @@ impl Propagator for HypercubeLinearPropagator { // does not appear in the linear inequality. Since the slack is negative, we // can propagate that predicate to false. - let conjunction: PropositionalConjunction = self - .linear - .terms() - .map(|term| predicate![term >= context.lower_bound(&term)]) - .chain( - self.hypercube_predicates - .iter() - .copied() - .filter(|&predicate| predicate != predicate_in_hypercube), - ) - .collect(); - - context.post(!predicate_in_hypercube, conjunction, &self.inference_code)?; + context.post(!predicate_in_hypercube, 0_u64)?; } else if let Some(term_to_propagate) = maybe_term { // The slack is at least 0, but it may be that the linear could propagate // something weaker than `!predicate_in_hypercube`. @@ -302,7 +326,10 @@ impl Propagator for HypercubeLinearPropagator { return Ok(()); } - let bound_i64 = slack + i64::from(context.lower_bound(&term_to_propagate)); + let bound_in_state = context.lower_bound(&term_to_propagate); + let bound_in_hypercube = self.hypercube.lower_bound(&term_to_propagate); + let bound_i64 = slack + i64::from(i32::max(bound_in_state, bound_in_hypercube)); + let bound = match i32::try_from(bound_i64) { Ok(bound) => bound, Err(_) if bound_i64.is_negative() => todo!( @@ -313,23 +340,7 @@ impl Propagator for HypercubeLinearPropagator { Err(_) => return Ok(()), }; - let conjunction: PropositionalConjunction = self - .linear - .terms() - .map(|term| predicate![term >= context.lower_bound(&term)]) - .chain( - self.hypercube_predicates - .iter() - .copied() - .filter(|&predicate| predicate != predicate_in_hypercube), - ) - .collect(); - - context.post( - predicate![term_to_propagate <= bound], - conjunction, - &self.inference_code, - )?; + context.post(predicate![term_to_propagate <= bound], 0_u64)?; } } @@ -370,7 +381,12 @@ impl Propagator for HypercubeLinearPropagator { let lower_bound_terms = self .linear .terms() - .map(|term| i64::from(context.lower_bound(&term))) + .map(|term| { + let bound_in_state = context.lower_bound(&term); + let bound_in_hypercube = self.hypercube.lower_bound(&term); + + i64::from(i32::max(bound_in_state, bound_in_hypercube)) + }) .sum::(); let slack = i64::from(self.linear.bound()) - lower_bound_terms; @@ -391,13 +407,15 @@ impl Propagator for HypercubeLinearPropagator { ) .collect::(); - context.post(!unassigned_predicate, reason, &self.inference_code)?; + context.post(!unassigned_predicate, (reason, &self.inference_code))?; } else if let Some(term) = self .linear .term_for_domain(unassigned_predicate.get_domain()) { - let term_lower_bound = context.lower_bound(&term); - let new_upper_bound = match i32::try_from(slack + i64::from(term_lower_bound)) { + let bound_in_state = context.lower_bound(&term); + let bound_in_hypercube = self.hypercube.lower_bound(&term); + let bound_i64 = slack + i64::from(i32::max(bound_in_state, bound_in_hypercube)); + let new_upper_bound = match i32::try_from(slack + bound_i64) { Ok(bound) => bound, Err(_) => return Ok(()), }; @@ -412,8 +430,7 @@ impl Propagator for HypercubeLinearPropagator { context.post( predicate![term <= new_upper_bound], - reason, - &self.inference_code, + (reason, &self.inference_code), )?; } } else { @@ -691,4 +708,77 @@ mod tests { assert_eq!(state.upper_bound(z2), 6); assert_eq!(state.upper_bound(z3), 6); } + + #[test] + fn single_predicate_in_hypercube_with_trivially_false_linear_triggers() { + let mut state = State::default(); + + let x = state.new_interval_variable(0, 10, Some("x".into())); + + let hypercube = Hypercube::new([predicate![x >= 2]]).expect("not inconsistent"); + + let linear = LinearInequality::trivially_false(); + + let constraint_tag = state.new_constraint_tag(); + let _ = state.add_propagator(HypercubeLinearConstructor { + hypercube, + linear, + constraint_tag, + }); + + assert!(state.propagate_to_fixed_point().is_ok()); + assert_eq!(state.upper_bound(x), 1); + } + + #[test] + fn slack_should_be_hypercube_linear_slack() { + let mut state = State::default(); + + let x = state.new_interval_variable(2, 10, Some("x".into())); + + let hypercube = Hypercube::new([predicate![x >= 4]]).expect("not inconsistent"); + + let linear = + LinearInequality::new([(NonZero::new(1).unwrap(), x)], 3).expect("not trivially false"); + + let constraint_tag = state.new_constraint_tag(); + let _ = state.add_propagator(HypercubeLinearConstructor { + hypercube, + linear, + constraint_tag, + }); + + assert!(state.propagate_to_fixed_point().is_ok()); + assert_eq!(state.upper_bound(x), 3); + } + + #[test] + fn hypercube_is_taken_into_slack_calculation() { + let mut state = State::default(); + + let x = state.new_interval_variable(0, 10, None); + let y = state.new_interval_variable(0, 10, None); + let z = state.new_interval_variable(-6, 6, None); + + let hypercube = Hypercube::from_single_predicate(predicate![z <= -2]); + let linear = LinearInequality::new( + [ + (NonZero::new(1).unwrap(), x), + (NonZero::new(1).unwrap(), y), + (NonZero::new(-1).unwrap(), z), + ], + 0, + ) + .expect("not trivially satisfiable"); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator(HypercubeLinearConstructor { + hypercube, + linear, + constraint_tag, + }); + + assert!(state.propagate_to_fixed_point().is_ok()); + assert_eq!(state.lower_bound(z), -1); + } } diff --git a/pumpkin-crates/core/src/hypercube_linear/resh_strategy.rs b/pumpkin-crates/core/src/hypercube_linear/resh_strategy.rs new file mode 100644 index 000000000..9b28409bc --- /dev/null +++ b/pumpkin-crates/core/src/hypercube_linear/resh_strategy.rs @@ -0,0 +1,282 @@ +use std::num::NonZero; + +use dyn_clone::DynClone; +use itertools::Itertools; +use log::trace; + +use crate::create_statistics_struct; +use crate::hypercube_linear::LinearInequality; +use crate::hypercube_linear::conflict_state::ConflictState; +use crate::hypercube_linear::explanation::HypercubeLinearExplanation; +use crate::hypercube_linear::trail_view::TrailView; +use crate::hypercube_linear::trail_view::affine_lower_bound_at; +use crate::predicate; +use crate::predicates::Predicate; +use crate::statistics::Statistic; +use crate::statistics::StatisticLogger; + +pub(crate) trait ResHStrategy: DynClone + std::fmt::Debug { + fn apply( + &mut self, + state: &mut ConflictState, + trail: &mut dyn TrailView, + trail_position: usize, + pivot: Predicate, + explanation: HypercubeLinearExplanation, + ); + + fn log_statistics(&self, _logger: StatisticLogger) {} +} + +dyn_clone::clone_trait_object!(ResHStrategy); + +// ======== StandardResH ======== + +create_statistics_struct!(StandardResHStatistics { + num_propositional_resolutions_use_explanation_linear: usize, +}); + +#[derive(Clone, Debug, Default)] +pub(crate) struct StandardResH { + statistics: StandardResHStatistics, +} + +impl ResHStrategy for StandardResH { + fn apply( + &mut self, + state: &mut ConflictState, + trail: &mut dyn TrailView, + trail_position: usize, + pivot: Predicate, + mut explanation: HypercubeLinearExplanation, + ) { + let linear_propagated_pivot_to_false = explanation.iter_predicates().any(|p| p == !pivot); + + let linear_slack_is_negative = if let Some(linear) = explanation.linear() { + compute_linear_slack_at_trail_position(trail, linear, trail_position - 1) < 0 + } else { + true + }; + + let can_substitute_with_explanation_linear = + linear_propagated_pivot_to_false && linear_slack_is_negative; + + if state.conflicting_linear.is_trivially_false() && can_substitute_with_explanation_linear { + // If the conflicting linear is a clause, then we do not need to clausify + // the explanation. Instead, the linear of the conflicting constraint + // becomes the linear of the explanation and the hypercube of the conflict + // is extended with the hypercube of the conflict. + + trace!( + "since the linear in the conflict is trivially false, use linear from explanation" + ); + + for predicate in explanation.iter_predicates() { + let truth_value = trail + .truth_value_at(predicate, trail_position) + .expect("all predicates in explanation hypercube are assigned"); + + if !truth_value { + continue; + } + + state.add_hypercube_predicate(trail, predicate); + } + + let linear = explanation.take_linear(); + state.explain_linear(trail, &linear, trail_position - 1); + state.conflicting_linear = linear; + + self.statistics + .num_propositional_resolutions_use_explanation_linear += 1; + } else { + let clausal_explanation = explanation.into_clause(trail, pivot, trail_position); + + trace!( + "clausal explanation: {}", + clausal_explanation.iter().format(" & ") + ); + for predicate in clausal_explanation { + let truth_value = trail + .truth_value_at(predicate, trail_position) + .expect("all predicates in explanation hypercube are assigned"); + + if !truth_value { + continue; + } + + state.add_hypercube_predicate(trail, predicate); + } + } + } + + fn log_statistics(&self, logger: StatisticLogger) { + self.statistics.log(logger); + } +} + +/// A [`ResHStrategy`] that weakens the conflict and explanation linear until both are the same. +#[derive(Clone, Debug, Default)] +pub(super) struct MiddlingResH { + backup: StandardResH, + additional_predicates_buffer: Vec, +} + +impl ResHStrategy for MiddlingResH { + fn apply( + &mut self, + state: &mut ConflictState, + trail: &mut dyn TrailView, + trail_position: usize, + pivot: Predicate, + explanation: HypercubeLinearExplanation, + ) { + if explanation.is_clause() || state.conflicting_linear.is_trivially_false() { + // In the case that either the conflict or the explanation is a clause, we + // use the backup propositional resolution operation. + self.backup + .apply(state, trail, trail_position, pivot, explanation); + return; + } + + let explanation_linear = explanation.linear().expect("explanation was not a clause"); + + let mut linear_terms: Vec<_> = state + .conflicting_linear + .terms() + .map(|term| { + ( + NonZero::new(term.scale).expect("scale is non zero"), + term.inner, + TermSource::Conflict, + ) + }) + .merge_by( + explanation_linear.terms().map(|term| { + ( + NonZero::new(term.scale).expect("scale is non zero"), + term.inner, + TermSource::Explanation, + ) + }), + |(_, x1, _), (_, x2, _)| x1 <= x2, + ) + .collect(); + + let mut weakened_conflict_bound = state.conflicting_linear.bound(); + let mut weakened_explanation_bound = explanation_linear.bound(); + + for i in 0..linear_terms.len() - 1 { + let (w1, x1, s1) = linear_terms[i]; + let (w2, x2, s2) = linear_terms[i + 1]; + + if x1 != x2 { + continue; + } + + assert_ne!( + s1, s2, + "a linear inequality never yields multiple terms for the same variable" + ); + + if w1 == w2 { + // If both weights are equal, then we do not need to weaken. + continue; + } + + let ( + target_weight, + variable, + actual_weight, + source_to_weaken, + index_to_weaken_in_terms, + ) = if w1.abs() > w2.abs() { + (w2.get(), x1, w1.get(), s1, i) + } else { + (w1.get(), x1, w2.get(), s2, i + 1) + }; + + assert!(target_weight.abs() < actual_weight.abs()); + assert_eq!(target_weight.is_positive(), actual_weight.is_positive()); + + // Determine what predicate will be weakened on. + let predicate_to_weaken_on = if target_weight.is_positive() { + let bound = trail.lower_bound_at_trail_position(variable, trail_position); + predicate![variable >= bound] + } else { + let bound = trail.upper_bound_at_trail_position(variable, trail_position); + predicate![variable <= bound] + }; + + // Make sure the predicate is added to the final hypercube. + self.additional_predicates_buffer + .push(predicate_to_weaken_on); + + let weaken_bound_by = if predicate_to_weaken_on.is_lower_bound_predicate() { + -predicate_to_weaken_on.get_right_hand_side() + * (actual_weight.abs() - target_weight.abs()) + } else { + predicate_to_weaken_on.get_right_hand_side() + * (actual_weight.abs() - target_weight.abs()) + }; + + linear_terms[index_to_weaken_in_terms].0 = + NonZero::new(target_weight).expect("all weights are non-zero"); + + match source_to_weaken { + TermSource::Conflict => { + weakened_conflict_bound += weaken_bound_by; + } + TermSource::Explanation => { + weakened_explanation_bound += weaken_bound_by; + } + } + } + + let new_conflicting_linear = LinearInequality::new( + linear_terms + .into_iter() + .map(|(weight, domain, _)| (weight, domain)) + .dedup(), + weakened_explanation_bound.max(weakened_conflict_bound), + ) + .expect("propositional resolution keeps linear conflicting"); + + state.explain_linear(trail, &new_conflicting_linear, trail_position); + state.conflicting_linear = new_conflicting_linear; + + // Add the explanation hypercube and the predicates introduced through weakening to + // the conflicting hypercube + for predicate in explanation + .iter_predicates() + .filter(|&p| p != !pivot) // Do not add the negation of propagated predicate + .chain(self.additional_predicates_buffer.drain(..)) + { + state.add_hypercube_predicate(trail, predicate); + } + } + + fn log_statistics(&self, logger: StatisticLogger) { + self.backup.log_statistics(logger.clone()); + } +} + +/// From which HL does a term come from. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum TermSource { + Conflict, + Explanation, +} + +fn compute_linear_slack_at_trail_position( + trail: &dyn TrailView, + linear: &LinearInequality, + trail_position: usize, +) -> i64 { + let lower_bound_terms = linear + .terms() + .map(|term| i64::from(affine_lower_bound_at(trail, term, trail_position))) + .sum::(); + + i64::from(linear.bound()) - lower_bound_terms +} diff --git a/pumpkin-crates/core/src/hypercube_linear/resolver.rs b/pumpkin-crates/core/src/hypercube_linear/resolver.rs new file mode 100644 index 000000000..53db2f714 --- /dev/null +++ b/pumpkin-crates/core/src/hypercube_linear/resolver.rs @@ -0,0 +1,1441 @@ +use std::borrow::Cow; +use std::cell::RefCell; +use std::num::NonZero; +use std::rc::Rc; + +use itertools::Itertools; +use log::debug; +use log::trace; + +use crate::basic_types::StoredConflictInfo; +use crate::conflict_resolving::ConflictAnalysisContext; +use crate::conflict_resolving::ConflictResolver; +#[cfg(feature = "hl-checks")] +use crate::containers::HashMap; +use crate::create_statistics_struct; +use crate::hypercube_linear::BoundComparator; +use crate::hypercube_linear::BoundPredicate; +use crate::hypercube_linear::Hypercube; +use crate::hypercube_linear::HypercubeLinearConstructor; +use crate::hypercube_linear::LinearInequality; +use crate::hypercube_linear::Trace; +use crate::hypercube_linear::conflict_state::ConflictState; +use crate::hypercube_linear::conflict_state::predicate_applies_to_term; +use crate::hypercube_linear::explanation::HypercubeLinear; +use crate::hypercube_linear::explanation::HypercubeLinearExplanation; +use crate::hypercube_linear::resh_strategy::MiddlingResH; +use crate::hypercube_linear::resh_strategy::ResHStrategy; +use crate::hypercube_linear::resh_strategy::StandardResH; +use crate::hypercube_linear::trail_view::TrailView; +use crate::hypercube_linear::trail_view::affine_lower_bound_at; +use crate::hypercube_linear::trail_view::affine_upper_bound_at; +use crate::math::num_ext::NumExt; +use crate::predicate; +use crate::predicates::Predicate; +#[cfg(feature = "hl-checks")] +use crate::proof::ConstraintTag; +use crate::propagation::ExplanationContext; +use crate::state::Conflict; +use crate::state::EmptyDomainConflict; +use crate::state::State; +use crate::statistics::Statistic; +use crate::statistics::StatisticLogger; +use crate::variables::AffineView; +use crate::variables::DomainId; +use crate::variables::TransformableVariable; + +create_statistics_struct!(ResolverStatistics { + num_successful_fourier_resolutions: usize, + num_integer_overflow_errors: usize, + num_conflicts: usize, + num_learned_clauses: usize, + num_learned_hls: usize, + num_propositional_resolutions: usize, + num_skipped_propositional_resolutions: usize, +}); + +#[derive(Clone, Debug)] +pub struct HypercubeLinearResolver { + state: ConflictState, + prop_resolver: Box, + + /// The statistics gathered by the resolver. + statistics: ResolverStatistics, + + /// True if the names are written to the trace, false if not. + logged_variable_names: bool, + + /// All learned constraints with their constraint tag. + /// + /// Used to detect when re-learning the same constraint again. + #[cfg(feature = "hl-checks")] + learned_constraints: HashMap<(Hypercube, LinearInequality), ConstraintTag>, +} + +impl HypercubeLinearResolver { + pub fn new(trace: Trace) -> Self { + let proof_file = Rc::new(RefCell::new(trace)); + Self { + state: ConflictState::new(Rc::clone(&proof_file)), + prop_resolver: Box::new(StandardResH::default()), + statistics: Default::default(), + logged_variable_names: false, + #[cfg(feature = "hl-checks")] + learned_constraints: Default::default(), + } + } + + pub fn with_middling_resh(trace: Trace) -> Self { + let proof_file = Rc::new(RefCell::new(trace)); + Self { + state: ConflictState::new(Rc::clone(&proof_file)), + prop_resolver: Box::new(MiddlingResH::default()), + statistics: Default::default(), + logged_variable_names: false, + #[cfg(feature = "hl-checks")] + learned_constraints: Default::default(), + } + } +} + +impl Default for HypercubeLinearResolver { + fn default() -> Self { + HypercubeLinearResolver::new(Trace::discard()) + } +} + +impl ConflictResolver for HypercubeLinearResolver { + fn resolve_conflict(&mut self, context: &mut ConflictAnalysisContext) { + if !self.logged_variable_names { + self.state + .proof_file + .borrow_mut() + .write_variables(context.state); + self.logged_variable_names = true; + } + + debug!("Resolving conflict with hypercube linear resolution"); + + self.statistics.num_conflicts += 1; + + let conflict = match context.solver_state.get_conflict_info() { + StoredConflictInfo::Propagator(conflict) => conflict.into(), + StoredConflictInfo::EmptyDomain(conflict) => conflict.into(), + _ => unreachable!("can only resolve empty domain or propagator conflicts"), + }; + + self.resolve_conflict_impl(context, conflict); + } + + fn log_statistics(&self, logger: StatisticLogger) { + self.statistics.log(logger.clone()); + self.prop_resolver.log_statistics(logger); + } +} + +#[derive(Clone)] +pub(crate) struct LearnedHypercubeLinear { + pub(crate) hypercube: Hypercube, + pub(crate) linear: LinearInequality, + propagates_at: usize, +} + +impl HypercubeLinearResolver { + fn resolve_conflict_impl(&mut self, context: &mut ConflictAnalysisContext, conflict: Conflict) { + let learned_constraint = self.learn_hypercube_linear(context.state, conflict); + let constraint_tag = context.state.new_constraint_tag(); + + #[cfg(feature = "hl-checks")] + self.assert_new_constraint(learned_constraint.clone(), constraint_tag); + + debug!( + "Learned {} -> {}", + learned_constraint.hypercube, learned_constraint.linear, + ); + + self.state.proof_file.borrow_mut().deduction( + constraint_tag, + learned_constraint.hypercube.iter_predicates(), + learned_constraint.linear.terms(), + learned_constraint.linear.bound(), + context.state.get_checkpoint(), + learned_constraint.propagates_at, + ); + + context.restore_to(learned_constraint.propagates_at); + + let handle = context.state.add_propagator(HypercubeLinearConstructor { + hypercube: learned_constraint.hypercube, + linear: learned_constraint.linear, + constraint_tag, + }); + + debug!( + " with ID = {:?} and tag = {constraint_tag:?}", + handle.propagator_id(), + ); + } + + /// Learns a conflicting hypercube linear that would have propagated at an earlier checkpoint. + fn learn_hypercube_linear( + &mut self, + state: &mut State, + conflict: Conflict, + ) -> LearnedHypercubeLinear { + let (initial_predicates, conflicting_linear) = + self.collect_initial_conflict(state, conflict); + self.run_resolution(state, initial_predicates, conflicting_linear) + } + + /// Set up the initial conflict from `initial_predicates` and `conflicting_linear`, then run + /// the resolution loop. + /// + /// For empty-domain conflicts triggered by a hypercube linear, `conflicting_linear` contains + /// the linear part; [`Self::explain_linear`] seeds `predicates_to_explain` with the linear + /// term predicates at the conflict DL before the main loop starts. + /// + /// The returned [`LearnedHypercubeLinear`] contains the learned `(hypercube, linear)` pair. + pub(crate) fn run_resolution( + &mut self, + trail: &mut impl TrailView, + initial_predicates: impl IntoIterator, + conflicting_linear: LinearInequality, + ) -> LearnedHypercubeLinear { + assert!(self.state.hypercube_predicates_on_conflict_dl.is_empty()); + assert!(self.state.predicates_to_explain.is_empty()); + + self.state.conflicting_linear = conflicting_linear; + + // Seed predicates_to_explain with linear term predicates at the conflict DL. + // For hypercube-linear empty-domain conflicts this replaces the manual loop that + // was previously in collect_initial_conflict_from_empty_domain. For trivially-false + // linears (propagator / clausal conflicts) this is a no-op. + let conflict_tp = trail.current_trail_position(); + let linear_for_explain = self.state.conflicting_linear.clone(); + self.state + .explain_linear(trail, &linear_for_explain, conflict_tp); + + for predicate in initial_predicates { + self.state.add_hypercube_predicate(trail, predicate); + } + + self.run_resolution_loop(trail) + } + + fn run_resolution_loop(&mut self, trail: &mut impl TrailView) -> LearnedHypercubeLinear { + let mut trail_position = usize::MAX; + + loop { + trace!("--------- new iteration in conflict analysis",); + + trace!( + "conflict constraint: {} -> {}", + self.state + .working_hypercube + .iter_predicates() + .chain(self.state.hypercube_predicates_on_conflict_dl.iter()) + .format(" & "), + self.state.conflicting_linear, + ); + + trace!( + "to explain: {}", + self.state + .predicates_to_explain + .iter() + .map(|p| format!("{p} @ {}", trail.trail_position(p).unwrap())) + .format(", "), + ); + + #[cfg(feature = "hl-checks")] + self.assert_loop_invariants(trail, trail_position); + + if let Some(dl) = self.will_propagate_on_previous_dl(trail) { + return self.extract_learned_hypercube_linear(trail, dl); + } + + let pivot = self + .state + .predicates_to_explain + .pop() + .expect("there are at least two predicates to explain"); + + trail_position = { + let tp = trail + .trail_position(pivot) + .expect("all predicates are true"); + + assert!( + trail_position >= tp, + "last_tp = {trail_position}, tp = {tp}" + ); + + tp + }; + + trace!("applying HL resolution on {pivot} @ {trail_position}"); + trace!( + " trail predicate @ {trail_position} = {}", + trail.predicate_at_trail_position(trail_position) + ); + + if !self.state.contributes_to_conflict(pivot) { + trace!(" => no longer contributes, skipping"); + continue; + } + + self.state.proof_file.borrow_mut().intermediate_deduction( + self.state + .working_hypercube + .iter_predicates() + .chain(self.state.hypercube_predicates_on_conflict_dl.iter()), + self.state.conflicting_linear.terms(), + self.state.conflicting_linear.bound(), + ); + + let explanation = self.explain(trail, pivot); + trace!("explanation = {explanation}"); + + self.resolve(trail, trail_position, pivot, explanation); + self.simplify_conflict(); + } + } + + /// Simplify the conflicting hypercube linear given any equalities in the conflicting hypercube. + fn simplify_conflict(&mut self) { + let equality_predicates = self + .state + .working_hypercube + .iter_predicates() + .chain(self.state.hypercube_predicates_on_conflict_dl.iter()) + .filter_map(|predicate| { + if predicate.is_equality_predicate() { + Some((predicate.get_domain(), predicate.get_right_hand_side())) + } else { + None + } + }) + .collect::>(); + + for (domain, value) in equality_predicates { + let Some(term) = self.state.conflicting_linear.term_for_domain(domain) else { + continue; + }; + + let comparator = match term.scale.is_positive() { + true => BoundComparator::LowerBound, + false => BoundComparator::UpperBound, + }; + + self.state.conflicting_linear = std::mem::take(&mut self.state.conflicting_linear) + .weaken_to_zero(BoundPredicate { + domain, + comparator, + value, + }) + .expect("does not weaken to trivially satisfiable"); + } + } + + fn resolve( + &mut self, + trail: &mut impl TrailView, + trail_position: usize, + pivot: Predicate, + explanation: HypercubeLinearExplanation, + ) { + if let HypercubeLinearExplanation::Proper(hl) = &explanation { + match self.fourier_resolve(trail, trail_position, pivot, hl) { + Ok(()) => { + self.statistics.num_successful_fourier_resolutions += 1; + + #[cfg(feature = "hl-checks")] + assert!( + self.state + .conflicting_linear + .term_for_domain(pivot.get_domain()) + .is_none(), + "fourier resolve succeeded but pivot domain still in conflicting linear" + ); + } + Err(FourierError::NoVariableElimination) => {} + Err(FourierError::ResultOfEliminationTriviallySatisfiable) => { + panic!("should this happen?") + } + Err(FourierError::IntegerOverflow) => { + self.statistics.num_integer_overflow_errors += 1; + } + } + } + + self.propositional_resolve(trail, trail_position, pivot, explanation); + } + + fn propositional_resolve( + &mut self, + trail: &mut impl TrailView, + trail_position: usize, + pivot: Predicate, + mut explanation: HypercubeLinearExplanation, + ) { + trace!("applying propositional resolution on {pivot}"); + + // No propositional resolution happens when the following are true: + // - the pivot does not imply a predicate in the current conflict hypercube, + // - the pivot does not contribute to the negative slack of the linear. + + let pivot_implies_predicate_in_conflict = self + .state + .hypercube_predicates_on_conflict_dl + .iter() + .any(|p| { + let p_tp = trail.trail_position(p).expect("all hypercube is satisfied"); + + pivot.implies(p) && p_tp == trail_position + }); + + let pivot_relevant_for_linear_slack = self + .state + .conflicting_linear + .terms() + .any(|t| predicate_applies_to_term(pivot, t)); + + if !pivot_implies_predicate_in_conflict && !pivot_relevant_for_linear_slack { + trace!(" => skipping propositional resolution"); + self.statistics.num_skipped_propositional_resolutions += 1; + + #[cfg(feature = "hl-checks")] + assert!( + !self + .state + .hypercube_predicates_on_conflict_dl + .contains(pivot), + "skipped propositional resolution but pivot still in conflict DL heap" + ); + + return; + } + + let pivot_as_bound = BoundPredicate::new(pivot); + + // Both the conflict and the explanation are weakened on the pivot. This ensures + // the propositional resolution removes all contribution of the pivot to the + // conflict in the linear inequalities. + if let Some(bound_predicate) = pivot_as_bound { + self.state + .hypercube_predicates_on_conflict_dl + .push(bound_predicate.into(), trail); + self.state.conflicting_linear = std::mem::take(&mut self.state.conflicting_linear) + .weaken_to_zero(bound_predicate) + .expect( + "weakening the conflict will never result in a trivially satisfiable linear", + ); + + trace!( + "weakened conflict constraint: {} -> {}", + self.state + .working_hypercube + .iter_predicates() + .chain(self.state.hypercube_predicates_on_conflict_dl.iter()) + .format(" & "), + self.state.conflicting_linear, + ); + + explanation = std::mem::take(&mut explanation) + .weaken_to_zero(!bound_predicate) + .expect("cannot weaken to trivially satisfiable"); + + trace!("weakened explanation: {explanation}"); + } + + self.statistics.num_propositional_resolutions += 1; + self.state + .hypercube_predicates_on_conflict_dl + .retain(|p| !pivot.implies(p)); + + #[cfg(feature = "hl-checks")] + assert!( + !self + .state + .hypercube_predicates_on_conflict_dl + .contains(pivot), + "pivot still in conflict DL heap after propositional resolution" + ); + + self.prop_resolver + .apply(&mut self.state, trail, trail_position, pivot, explanation); + } + + fn explain( + &mut self, + trail: &mut impl TrailView, + pivot: Predicate, + ) -> HypercubeLinearExplanation { + let explanation = trail.reason_for(pivot); + + match &explanation { + HypercubeLinearExplanation::Proper(hl) => { + trace!("explaining with HL"); + self.state.proof_file.borrow_mut().axiom( + hl.hypercube.iter_predicates(), + hl.linear.terms(), + hl.linear.bound(), + ); + } + HypercubeLinearExplanation::Conjunction(predicates) => { + trace!("explaining with clause"); + + // Add reason predicates (all except !pivot) to the hypercube. + for &predicate in predicates.iter().filter(|&&p| p != !pivot) { + self.state.add_hypercube_predicate(trail, predicate); + + #[cfg(feature = "hl-checks")] + { + let pivot_tp = trail.trail_position(pivot).expect("pivot is on trail"); + let tp = trail + .trail_position(predicate) + .expect("all predicates are true"); + assert!(pivot_tp >= tp, "pivot_tp = {pivot_tp}, tp = {tp}"); + } + } + + self.state + .proof_file + .borrow_mut() + .axiom(predicates.iter().copied(), [], -1); + } + } + + explanation + } + + /// Collects the initial conflict predicates and linear from the given [`Conflict`]. + /// + /// Returns the hypercube predicates and the conflicting linear. For empty-domain conflicts + /// triggered by a hypercube linear, only the hypercube predicates are returned here; the + /// linear term predicates are seeded into [`Self::predicates_to_explain`] by + /// [`Self::run_resolution`] via [`Self::explain_linear`]. + fn collect_initial_conflict( + &mut self, + state: &mut State, + conflict: Conflict, + ) -> (Vec, LinearInequality) { + match conflict { + Conflict::Propagator(propagator_conflict) => { + trace!("Converting propagator conflict to hypercube"); + + self.state.proof_file.borrow_mut().axiom( + propagator_conflict.conjunction.iter().copied(), + [], + -1, + ); + + ( + propagator_conflict.conjunction.into_iter().collect(), + LinearInequality::trivially_false(), + ) + } + Conflict::EmptyDomain(empty_domain_conflict) => { + self.collect_initial_conflict_from_empty_domain(state, empty_domain_conflict) + } + } + } + + /// See [`Self::collect_initial_conflict`]. + fn collect_initial_conflict_from_empty_domain( + &mut self, + state: &mut State, + empty_domain_conflict: EmptyDomainConflict, + ) -> (Vec, LinearInequality) { + let EmptyDomainConflict { + trigger_reason, + trigger_predicate, + .. + } = empty_domain_conflict; + + assert_eq!(state.truth_value(trigger_predicate), Some(false)); + + let trigger_reason = + trigger_reason.expect("cannot resolve conflict that was triggered by an assumption"); + + trace!("{trigger_predicate:?} caused an empty domain, computing conflict constraint"); + + // Check whether we can explain using a hypercube linear. If that is possible, use + // it. Otherwise, fall back to the clausal explanation. + if let Some(code) = state.reason_store.get_lazy_code(trigger_reason) { + let propagator_id = state.reason_store.get_propagator(trigger_reason); + let trail_position = state.trail_len() - 1; + + if let Some((hypercube, linear, _)) = state.propagators[propagator_id] + .explain_as_hypercube_linear( + code, + ExplanationContext::without_working_nogood( + &state.assignments, + trail_position, + &mut state.notification_engine, + ), + ) + { + trace!("constructing conflict from HL"); + self.state.proof_file.borrow_mut().axiom( + hypercube.iter_predicates(), + linear.terms(), + linear.bound(), + ); + + // Only the hypercube predicates are returned; the linear term predicates at + // the conflict DL are seeded by run_resolution via explain_linear. + let predicates = hypercube.iter_predicates().collect(); + return (predicates, linear); + } + } + + trace!("constructing conflict from clause"); + let mut clausal_conflict = vec![]; + let _ = state.reason_store.get_or_compute( + trigger_reason, + ExplanationContext::without_working_nogood( + &state.assignments, + // -1 is not necessary, conflicting trail entry is undone + state.trail_len(), + &mut state.notification_engine, + ), + &mut state.propagators, + &mut clausal_conflict, + trigger_predicate, + ); + clausal_conflict.push(!trigger_predicate); + + self.state + .proof_file + .borrow_mut() + .axiom(clausal_conflict.iter().copied(), [], -1); + + trace!("conflicting predicate = {trigger_predicate:?}"); + + if cfg!(feature = "hl-checks") { + let unsatisfied_predicates = clausal_conflict + .iter() + .copied() + .filter(|&predicate| state.truth_value(predicate) != Some(true)) + .collect::>(); + + if !unsatisfied_predicates.is_empty() { + for p in unsatisfied_predicates { + eprintln!(" - {p} = {:?}", state.truth_value(p)); + } + panic!("not all predicates in the conflict are satisfied"); + } + } + + (clausal_conflict, LinearInequality::trivially_false()) + } + + /// Build the learned hypercube linear from the current state of the resolver. + fn extract_learned_hypercube_linear( + &mut self, + trail: &impl TrailView, + propagates_at: usize, + ) -> LearnedHypercubeLinear { + let hypercube = std::mem::take(&mut self.state.working_hypercube) + .with_predicates(self.state.hypercube_predicates_on_conflict_dl.drain()) + .expect("can never encounter inconsistent hypercube"); + + let _ = self.state.predicates_to_explain.drain(); + + // Determine whether the linear can propagate something at some point. + // If not, replace it with a trivially false linear to save memory and + // registrations for bound events. + let root_trail_position = trail.trail_position_at_checkpoint(0); + let hl_slack_at_root = compute_hl_slack_at_trail_position( + trail, + &hypercube, + &self.state.conflicting_linear, + root_trail_position, + ); + + let linear = if hl_slack_at_root < 0 { + self.statistics.num_learned_clauses += 1; + + // Make sure to add in the inferences to the proof. + for term in self.state.conflicting_linear.terms() { + let term_lb = affine_lower_bound_at(trail, term, root_trail_position); + + self.state + .proof_file + .borrow_mut() + .axiom([predicate![term <= term_lb - 1]], [], -1); + } + + LinearInequality::trivially_false() + } else { + self.statistics.num_learned_hls += 1; + std::mem::take(&mut self.state.conflicting_linear) + }; + + LearnedHypercubeLinear { + hypercube, + linear, + propagates_at, + } + } + + /// Ensures the learned constraint is a new constraint, rather than a previously learned + /// one. + #[cfg(feature = "hl-checks")] + fn assert_new_constraint( + &mut self, + constraint: LearnedHypercubeLinear, + constraint_tag: ConstraintTag, + ) { + assert_eq!( + self.learned_constraints + .insert((constraint.hypercube, constraint.linear), constraint_tag), + None, + "relearned the same constraint" + ); + } + + fn fourier_resolve( + &mut self, + trail: &mut impl TrailView, + trail_position: usize, + pivot: Predicate, + explanation: &HypercubeLinear, + ) -> Result<(), FourierError> { + let maybe_term_in_conflicting = self + .state + .conflicting_linear + .term_for_domain(pivot.get_domain()); + let maybe_term_in_reason = explanation.linear.term_for_domain(pivot.get_domain()); + + let (weight_in_conflicting, weight_in_reason) = + match (maybe_term_in_conflicting, maybe_term_in_reason) { + (Some(term_in_conflicting), Some(term_in_reason)) + if term_in_conflicting.scale.is_positive() + != term_in_reason.scale.is_positive() => + { + (term_in_conflicting.scale, term_in_reason.scale) + } + + // Either the domain is not in one of the two constraints, or they don't have + // opposing signs. In both cases, we cannot perform fourier + // elimination on the specified domain. + _ => return Err(FourierError::NoVariableElimination), + }; + + let reason_hypercube_satisfied = explanation + .hypercube + .iter_predicates() + .all(|p| trail.truth_value_at(p, trail_position) == Some(true)); + + // This is important if the hypercube linear propagated when all but one hypercube bound + // was satisfied. + if !reason_hypercube_satisfied { + return Err(FourierError::NoVariableElimination); + } + + let contributes_to_conflict_in_linear = (weight_in_conflicting.is_positive() + && pivot.is_lower_bound_predicate()) + || (weight_in_conflicting.is_negative() && pivot.is_upper_bound_predicate()); + + // We should only do fourier elimination if the pivot predicate actually contributes to the + // conflict. Otherwise performing the combination will not remove any contribution to the + // conflict. + if !contributes_to_conflict_in_linear { + return Err(FourierError::NoVariableElimination); + } + + let conflict_slack = compute_linear_slack_at_trail_position( + trail, + &self.state.conflicting_linear, + trail_position, + ); + let reason_slack = + compute_linear_slack_at_trail_position(trail, &explanation.linear, trail_position); + + trace!("applying fourier elimination on {pivot}"); + trace!(" - slack conflict: {conflict_slack}"); + trace!(" - slack b: {reason_slack}"); + + let tightly_propagating_reason = if i64::from(weight_in_reason) * conflict_slack.abs() + > i64::from(weight_in_conflicting) * reason_slack + { + Cow::Borrowed(explanation) + } else { + compute_tightly_propagating_reason( + trail, + trail_position, + explanation, + reason_slack, + pivot.get_domain().scaled(weight_in_reason), + ) + }; + + trace!(" - tightly propagating: {tightly_propagating_reason}"); + + let tp_slack = compute_linear_slack_at_trail_position( + trail, + &tightly_propagating_reason.linear, + trail_position, + ); + trace!(" - slack: {tp_slack}"); + + // Extend the hypercube of the conflict with the predicates from the hypercube of + // the explanation. + for predicate in tightly_propagating_reason.hypercube.iter_predicates() { + self.state.add_hypercube_predicate(trail, predicate); + } + + self.state + .explain_linear(trail, &tightly_propagating_reason.linear, trail_position); + + let scale_reason = weight_in_conflicting.abs(); + let scale_conflict = tightly_propagating_reason + .linear + .term_for_domain(pivot.get_domain()) + .unwrap() + .scale + .abs(); + + let mut linear_terms = self + .state + .conflicting_linear + .terms() + .map(|term| { + term.scale + .checked_mul(scale_conflict) + .ok_or(FourierError::IntegerOverflow) + .map(|scaled_weight| (scaled_weight, term.inner)) + }) + .chain( + tightly_propagating_reason + .linear + .terms() + .map(|reason_term| { + reason_term + .scale + .checked_mul(scale_reason) + .ok_or(FourierError::IntegerOverflow) + .map(|scaled_weight| (scaled_weight, reason_term.inner)) + }), + ) + .collect::, _>>()?; + + let mut linear_rhs = self + .state + .conflicting_linear + .bound() + .checked_add( + tightly_propagating_reason + .linear + .bound() + .checked_mul(scale_reason) + .ok_or(FourierError::IntegerOverflow)?, + ) + .ok_or(FourierError::IntegerOverflow)?; + + // Normalize the linear component of the hypercube linear to hopefully avoid overflows in + // the future. + let normalize_by = linear_terms + .iter() + .map(|(weight, _)| *weight) + .chain(std::iter::once(linear_rhs)) + .reduce(gcd) + .unwrap_or(linear_rhs); + + linear_terms.iter_mut().for_each(|(weight, _)| { + *weight = ::div_ceil(*weight, normalize_by); + }); + linear_rhs = ::div_ceil(linear_rhs, normalize_by); + + self.state.conflicting_linear = LinearInequality::new( + linear_terms + .into_iter() + .map(|(weight, domain)| (NonZero::new(weight).unwrap(), domain)), + linear_rhs, + ) + .ok_or(FourierError::ResultOfEliminationTriviallySatisfiable)?; + + Ok(()) + } + + fn will_propagate_on_previous_dl(&self, trail: &impl TrailView) -> Option { + trace!("Testing propagation at previous dl"); + let current_dl = trail.current_checkpoint(); + let decision_levels = (0..current_dl).rev(); + + // Before we test whether we can backtrack, we test whether the predicates that are + // true at the current decision level cover at most one domain. If not, then we for + // sure cannot backtrack. + let mut d1 = None; + for p in self.state.hypercube_predicates_on_conflict_dl.iter() { + if d1 == None { + d1 = Some(p.get_domain()); + } else if d1 != Some(p.get_domain()) { + // If there are two different domains in the predicates that the + // current decision level, then we know for sure we cannot backjump. + return None; + } + } + + for decision_level in decision_levels { + trace!(" => testing dl = {decision_level}"); + let trail_position = trail.trail_position_at_checkpoint(decision_level); + + // TODO: Optimize this + let final_hypercube = self + .state + .working_hypercube + .clone() + .with_predicates(self.state.hypercube_predicates_on_conflict_dl.iter()) + .expect("no inconsistent hypercube"); + if !propagates_at( + trail, + trail_position, + &final_hypercube, + &self.state.conflicting_linear, + ) { + trace!(" => does not propagate"); + // The initial assumption is that the hypercube linear is conflicting, so it will be + // propagating at the current decision level. That means the first time it does not + // propagate is one decision level too far back. + + let backjump_dl = decision_level + 1; + + if backjump_dl < current_dl { + return Some(decision_level + 1); + } else { + return None; + } + } + } + + // At this point the constraint is propagating at decision level 0, since that is the last + // decision level tested in the loop. + Some(0) + } + + /// Assert loop invariants at the top of each resolution iteration. + #[cfg(feature = "hl-checks")] + fn assert_loop_invariants(&self, trail: &impl TrailView, trail_position: usize) { + // All predicates at the conflict DL must have the current checkpoint. + let current_cp = trail.current_checkpoint(); + for p in self.state.hypercube_predicates_on_conflict_dl.iter() { + assert_eq!( + trail.checkpoint_for_predicate(p), + Some(current_cp), + "predicate {p} in conflict DL heap has wrong checkpoint" + ); + } + for p in self.state.predicates_to_explain.iter() { + assert_eq!( + trail.checkpoint_for_predicate(p), + Some(current_cp), + "predicate {p} in predicates_to_explain has wrong checkpoint" + ); + } + // All predicates in the working hypercube must be at a strictly earlier checkpoint. + for p in self.state.working_hypercube.iter_predicates() { + let cp = trail + .checkpoint_for_predicate(p) + .expect("working hypercube predicate must be assigned"); + assert!( + cp > 0 && cp < current_cp, + "predicate {p} in working_hypercube has checkpoint {cp}, expected 0 < cp < {current_cp}" + ); + } + + // The working conflict must be genuinely violated: all hypercube predicates satisfied and + // linear has negative slack. + if trail_position == usize::MAX { + return; + } + + let last_tp_prev_cp = trail.trail_position_at_checkpoint(current_cp - 1); + assert!( + trail_position > last_tp_prev_cp, + "trail_position {trail_position} should be after previous checkpoint end {last_tp_prev_cp}" + ); + + let linear_slack = compute_linear_slack_at_trail_position( + trail, + &self.state.conflicting_linear, + trail_position, + ); + + if !linear_slack.is_negative() { + eprintln!("Bounds in conflicting linear:"); + for term in self.state.conflicting_linear.terms() { + let lb = affine_lower_bound_at(trail, term, trail_position); + eprintln!( + " - {} {} >= {} @ {:?}", + term.scale, + term.inner, + lb, + trail.trail_position(predicate![term >= lb]) + ); + } + panic!( + "conflicting constraint linear is not conflicting at trail position {trail_position}" + ); + } + + let unsatisfied = self + .state + .hypercube_predicates_on_conflict_dl + .iter() + .chain(self.state.working_hypercube.iter_predicates()) + .filter(|&p| trail.truth_value_at(p, trail_position) != Some(true)) + .collect::>(); + assert_eq!( + unsatisfied, + vec![], + "hypercube contains unsatisfied predicates at trail position {trail_position}" + ); + } +} + +/// Returns true if the given hypercube linear propagates at the given trail position. +fn propagates_at( + trail: &impl TrailView, + trail_position: usize, + hypercube: &Hypercube, + linear: &LinearInequality, +) -> bool { + // Get the predicates that are not assigned to true. + let unsatisfied_predicates_in_hypercube = hypercube + .iter_predicates() + .filter(|&predicate| trail.truth_value_at(predicate, trail_position) != Some(true)) + .collect::>(); + + if unsatisfied_predicates_in_hypercube.len() > 1 { + // If more than one predicate remains unassigned, we cannot do anything. + return false; + } + + let slack = compute_hl_slack_at_trail_position(trail, hypercube, linear, trail_position); + + if unsatisfied_predicates_in_hypercube.len() == 1 { + let unassigned_predicate = unsatisfied_predicates_in_hypercube[0]; + let domain_of_predicate = unassigned_predicate.get_domain(); + + if slack < 0 { + return true; + } else if let Some(term) = linear.term_for_domain(domain_of_predicate) { + let bound_in_state = affine_lower_bound_at(trail, term, trail_position); + let bound_in_hypercube = hypercube.lower_bound(&term); + let bound_i64 = i64::from(i32::max(bound_in_state, bound_in_hypercube)); + + let new_upper_bound = match i32::try_from(slack + bound_i64) { + Ok(bound) => bound, + Err(_) => return false, + }; + + let predicate_to_propagate = predicate![term <= new_upper_bound]; + let predicate_truth_value = + trail.truth_value_at(predicate_to_propagate, trail_position); + + return (!unassigned_predicate).implies(predicate_to_propagate) + && predicate_truth_value != Some(true); + } + } else { + assert!(unsatisfied_predicates_in_hypercube.is_empty()); + return linear_propagates_at(trail, trail_position, linear); + } + + false +} + +/// Returns true if the given linear propagates at the given trail position. +fn linear_propagates_at( + trail: &impl TrailView, + trail_position: usize, + conflicting_linear: &LinearInequality, +) -> bool { + if conflicting_linear.is_trivially_false() { + return true; + } + + let slack = compute_linear_slack_at_trail_position(trail, conflicting_linear, trail_position); + + for term in conflicting_linear.terms() { + let term_lower_bound = i64::from(affine_lower_bound_at(trail, term, trail_position)); + let new_term_upper_bound_i64 = slack + term_lower_bound; + let new_term_upper_bound = match i32::try_from(new_term_upper_bound_i64) { + Ok(bound) => bound, + // The upper bound is smaller than i32::MIN, which means this would propagate (even + // if we cannot perform the propagation due to our domains being 32-bit). + Err(_) if new_term_upper_bound_i64.is_negative() => return true, + // If we want to set the upper bound to a value larger than i32::MAX, + // it can never tighten the existing bound of `term_to_propagate`. + Err(_) => continue, + }; + + if new_term_upper_bound < affine_upper_bound_at(trail, term, trail_position) { + return true; + } + } + + false +} + +/// Use weakening to obtain a hypercube linear that propagates the given term without any rounding. +fn compute_tightly_propagating_reason<'expl>( + trail: &impl TrailView, + trail_position: usize, + original_reason: &'expl HypercubeLinear, + reason_slack: i64, + pivot_term: AffineView, +) -> Cow<'expl, HypercubeLinear> { + let pivot_term_upper_bound = + reason_slack + i64::from(affine_lower_bound_at(trail, pivot_term, trail_position)); + + if pivot_term_upper_bound % i64::from(pivot_term.scale) == 0 { + return Cow::Borrowed(original_reason); + } + + let divisor = pivot_term.scale.abs(); + + let bounds_to_weaken: Vec<_> = original_reason + .linear + .terms() + .filter_map(|term| { + // If the weight of the term is divisible by the weight of the pivot term, then we + // keep it in the linear part. Otherwise, it is weakened on. + if term.scale % divisor == 0 { + return None; + } + + let bound = affine_lower_bound_at(trail, term, trail_position); + Some(BoundPredicate::new(predicate![term >= bound]).expect("only doing bounds")) + }) + .collect(); + + let mut tightly_propagating_reason = original_reason.clone(); + + for bound in bounds_to_weaken { + tightly_propagating_reason.hypercube = + std::mem::take(&mut tightly_propagating_reason.hypercube) + .with_predicate(bound.into()) + .expect("bound is true and original is true so hypercube is not inconsistent"); + + let term = tightly_propagating_reason + .linear + .term_for_domain(bound.domain) + .expect("the bound is computed based on terms"); + + let num_weakenings = (term.scale % divisor).abs(); + + tightly_propagating_reason.linear = tightly_propagating_reason + .linear + .weaken(bound, num_weakenings) + .expect("never becomes trivially satisfiable"); + } + + tightly_propagating_reason.linear.divide(divisor); + + Cow::Owned(tightly_propagating_reason) +} + +fn compute_hl_slack_at_trail_position( + trail: &impl TrailView, + hypercube: &Hypercube, + linear: &LinearInequality, + trail_position: usize, +) -> i64 { + let lower_bound_terms = linear + .terms() + .map(|term| { + let state_lb = affine_lower_bound_at(trail, term, trail_position); + let hypercube_lb = hypercube.lower_bound(&term); + i64::from(i32::max(state_lb, hypercube_lb)) + }) + .sum::(); + + i64::from(linear.bound()) - lower_bound_terms +} + +fn compute_linear_slack_at_trail_position( + trail: &impl TrailView, + linear: &LinearInequality, + trail_position: usize, +) -> i64 { + let lower_bound_terms = linear + .terms() + .map(|term| i64::from(affine_lower_bound_at(trail, term, trail_position))) + .sum::(); + + i64::from(linear.bound()) - lower_bound_terms +} + +enum FourierError { + NoVariableElimination, + ResultOfEliminationTriviallySatisfiable, + IntegerOverflow, +} + +// Taken from https://docs.rs/num-integer/latest/src/num_integer/lib.rs.html#420-422 +#[allow(unused, reason = "experimentation")] +fn gcd(a: i32, b: i32) -> i32 { + let mut m = a; + let mut n = b; + if m == 0 || n == 0 { + return (m | n).abs(); + } + + // find common factors of 2 + let shift = (m | n).trailing_zeros(); + + // The algorithm needs positive numbers, but the minimum value + // can't be represented as a positive one. + // It's also a power of two, so the gcd can be + // calculated by bitshifting in that case + + // Assuming two's complement, the number created by the shift + // is positive for all numbers except gcd = abs(min value) + // The call to .abs() causes a panic in debug mode + if m == i32::MIN || n == i32::MIN { + let i: i32 = 1 << shift; + return i.abs(); + } + + // guaranteed to be positive now, rest like unsigned algorithm + m = m.abs(); + n = n.abs(); + + // divide n and m by 2 until odd + m >>= m.trailing_zeros(); + n >>= n.trailing_zeros(); + + while m != n { + if m > n { + m -= n; + m >>= m.trailing_zeros(); + } else { + n -= m; + n >>= n.trailing_zeros(); + } + } + m << shift +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::conjunction; + use crate::hypercube_linear::explanation::HypercubeLinear; + use crate::hypercube_linear::fake_trail::FakeTrail; + use crate::linear_inequality; + use crate::predicate; + + /// One Fourier elimination step (on x) produces y + z ≤ 7. + /// + /// Conflict: x + y ≤ 9, with x ≥ 5 (at DL 2) and y ≥ 1 (at DL 2) both violated. + /// Reason for x ≥ 5: {} → −x + z ≤ −2 (z ≥ 3 at DL 1 implies z − x ≤ −2, i.e. x ≥ z+2 ≥ 5). + /// + /// Fourier combination: (x + y ≤ 9) + (−x + z ≤ −2) = y + z ≤ 7. + /// + /// After Fourier: only y ≥ 1 remains on the conflict DL; z ≥ 3 is at an earlier DL. + /// The constraint {z ≥ 3, y ≥ 1} → y + z ≤ 7 is unit-propagating at DL 1 (slack = −1), + /// so we backjump there and the learned constraint is ({z ≥ 3, y ≥ 1}, y + z ≤ 7). + #[test_log::test] + fn one_fourier_step_yields_y_plus_z_le_7() { + let mut trail_builder = FakeTrail::builder(); + + let x = trail_builder.domain(0, 10); + let y = trail_builder.domain(0, 10); + let z = trail_builder.domain(0, 10); + + let mut trail = trail_builder + .decide(predicate![z >= 3]) + .decide(predicate![y >= 1]) + .propagate( + predicate![x >= 5], + HypercubeLinear::from(linear_inequality!(-1 x + 1 z <= -2)), + ) + .build(); + + let mut resolver = HypercubeLinearResolver::default(); + let result = resolver.run_resolution( + &mut trail, + [predicate![x >= 5], predicate![y >= 1]], + linear_inequality!(1 x + 1 y <= 5), + ); + + // The Fourier step eliminates x: conflict becomes y + z ≤ 3. + // HL-slack at DL0 (y≥1, z≥3 in hypercube, bound=3): 3−1−3 = −1 < 0 → trivially_false. + // Hypercube should contain z ≥ 3 (from DL 1) and y ≥ 1 (from DL 2). + let expected_hypercube = + Hypercube::new([predicate![y >= 1], predicate![z >= 3]]).expect("not inconsistent"); + + assert_eq!(result.hypercube, expected_hypercube); + assert!(result.linear.is_trivially_false()); + } + + /// Propositional resolution on x ≥ 5 adds its reason predicate y ≥ 3 (DL 1) to the + /// working hypercube. A second conflict-DL predicate w ≥ 2 keeps two domains at the conflict + /// DL so that `will_propagate_on_previous_dl` cannot terminate early. + /// + /// After resolving x ≥ 5 (adding y ≥ 3 to the working hypercube), only w ≥ 2 remains on + /// the conflict DL. The constraint {y ≥ 3, w ≥ 2} → trivially_false propagates at DL 1 + /// (HL-slack = −1 with one unsatisfied predicate), so we backjump there. + #[test_log::test] + fn propositional_resolution_adds_conjunction_reason_to_working_hypercube() { + let mut trail_builder = FakeTrail::builder(); + + let x = trail_builder.domain(0, 10); + let y = trail_builder.domain(0, 10); + let w = trail_builder.domain(0, 10); + + let mut trail = trail_builder + .decide(predicate![y >= 3]) + .decide(predicate![w >= 2]) + .propagate( + predicate![x >= 5], + conjunction!([y >= 3] & [x <= 4]), // !pivot + ) + .build(); + + // Conflict: trivially_false linear with two conflict-DL predicates. + // Two different domains → multi-domain check returns None → resolution proceeds. + let mut resolver = HypercubeLinearResolver::default(); + let result = resolver.run_resolution( + &mut trail, + [predicate![x >= 5], predicate![w >= 2]], + LinearInequality::trivially_false(), + ); + + let expected_hypercube = + Hypercube::new([predicate![y >= 3], predicate![w >= 2]]).expect("not inconsistent"); + + assert_eq!(result.hypercube, expected_hypercube); + assert!(result.linear.is_trivially_false()); + } + + #[test_log::test] + fn do_not_resolve_on_too_strong_a_pivot_in_hypercube() { + let mut trail_builder = FakeTrail::builder(); + + let x = trail_builder.domain(0, 10); + let y = trail_builder.domain(0, 10); + let z = trail_builder.domain(0, 5); + + let mut trail = trail_builder + .decide(predicate![x >= 2]) + .decide(predicate![y >= 3]) + .propagate(predicate![z >= 5], conjunction!([y >= 3] & [z <= 4])) + .propagate(predicate![x >= 4], conjunction!([y >= 3] & [x <= 3])) + .build(); + + let mut resolver = HypercubeLinearResolver::default(); + let result = resolver.run_resolution( + &mut trail, + [predicate![x >= 2]], + linear_inequality!(1 y + 3 z <= 15), + ); + + assert_eq!( + result.hypercube, + Hypercube::new([predicate![x >= 2], predicate![y >= 3]]).expect("not inconsistent") + ); + assert!(result.linear.is_trivially_false()); + } + + #[test_log::test] + fn do_not_resolve_on_too_strong_a_pivot_when_stronger_bound_used_in_linear() { + let mut trail_builder = FakeTrail::builder(); + + let x = trail_builder.domain(0, 10); + let y = trail_builder.domain(0, 10); + let z = trail_builder.domain(0, 5); + + let mut trail = trail_builder + .decide(predicate![x >= 2]) + .decide(predicate![y >= 3]) + .propagate(predicate![z >= 5], conjunction!([y >= 3] & [z <= 4])) + .propagate(predicate![x >= 4], linear_inequality!(-1 x + 1 y <= -1)) + .build(); + + let mut resolver = HypercubeLinearResolver::default(); + let result = resolver.run_resolution( + &mut trail, + [predicate![x >= 2]], + linear_inequality!(1 x + 1 y + 3 z <= 19), + ); + + assert_eq!( + result.hypercube, + Hypercube::from_single_predicate(predicate![x >= 2]), + ); + assert_eq!(result.linear, linear_inequality!(2 y + 3 z <= 18)); + } + + #[test_log::test] + fn propositional_resolution_may_also_weaken_linear_from_explanation() { + let mut trail_builder = FakeTrail::builder(); + + let x = trail_builder.domain(0, 5); + let y = trail_builder.domain(-5, 5); + let z = trail_builder.domain(-5, 5); + + let mut trail = trail_builder + .decide(predicate![z >= 0]) + .decide(predicate![y >= 2]) + .propagate( + predicate![x >= 3], + HypercubeLinear { + hypercube: Hypercube::from_single_predicate(predicate![x <= 2]), + linear: linear_inequality!(2 y + 1 z <= 3), + }, + ) + .build(); + + let mut resolver = HypercubeLinearResolver::with_middling_resh(Trace::discard()); + let result = resolver.run_resolution( + &mut trail, + conjunction!([x >= 3]), + linear_inequality!(1 y + 1 z <= 1), + ); + + assert_eq!( + result.hypercube, + Hypercube::from_single_predicate(predicate![y >= 2]), + ); + assert_eq!(result.linear, linear_inequality!(1 y + 1 z <= 1)); + } + + #[test_log::test] + fn middling_resh_works_with_negative_signs() { + let mut trail_builder = FakeTrail::builder(); + + let x = trail_builder.domain(0, 5); + let y = trail_builder.domain(-5, 5); + let z = trail_builder.domain(-5, 5); + + let mut trail = trail_builder + .decide(predicate![z >= 0]) + .decide(predicate![y <= -2]) + .propagate( + predicate![x >= 3], + HypercubeLinear { + hypercube: Hypercube::from_single_predicate(predicate![x <= 2]), + linear: linear_inequality!(-2 y + 1 z <= 3), + }, + ) + .build(); + + let mut resolver = HypercubeLinearResolver::with_middling_resh(Trace::discard()); + let result = resolver.run_resolution( + &mut trail, + conjunction!([x >= 3]), + linear_inequality!(-1 y + 1 z <= 1), + ); + + assert_eq!( + result.hypercube, + Hypercube::from_single_predicate(predicate![y <= -2]), + ); + assert_eq!(result.linear, linear_inequality!(-1 y + 1 z <= 1)); + } +} diff --git a/pumpkin-crates/core/src/hypercube_linear/trace.rs b/pumpkin-crates/core/src/hypercube_linear/trace.rs new file mode 100644 index 000000000..180c9bcb8 --- /dev/null +++ b/pumpkin-crates/core/src/hypercube_linear/trace.rs @@ -0,0 +1,158 @@ +use std::fs::File; +use std::io::BufWriter; +use std::io::Write; +use std::num::NonZero; + +use itertools::Itertools; + +use crate::predicates::Predicate; +use crate::proof::ConstraintTag; +use crate::state::State; +use crate::variables::AffineView; +use crate::variables::DomainId; + +/// A wrapper around a proof file +#[derive(Debug)] +pub struct Trace { + /// The proof file. + proof_file: Option>, + + options: TraceOptions, +} + +/// How to trace the hypercube linear resolver. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct TraceOptions { + pub include_intermediate_steps: bool, +} + +impl Trace { + /// Trace to the given file. + pub fn to_file(file: File, options: TraceOptions) -> Trace { + Trace { + proof_file: Some(BufWriter::new(file)), + options, + } + } + + /// Discard what is written to the trace. + pub fn discard() -> Trace { + Trace { + proof_file: None, + options: TraceOptions::default(), + } + } + + pub fn write_variables(&mut self, state: &State) { + let Some(writer) = self.proof_file.as_mut() else { + return; + }; + + for (domain_id, name) in state.variable_names.named_domains() { + if is_likely_a_constant(domain_id, state) { + continue; + } + + writeln!(writer, "v {domain_id} {name}").expect("failed to write to trace"); + } + } + + /// Log an axiom. + pub fn axiom( + &mut self, + hypercube: impl IntoIterator, + linear_terms: impl IntoIterator>, + linear_rhs: i32, + ) { + let Some(writer) = self.proof_file.as_mut() else { + return; + }; + + writeln!( + writer, + "i {h} -> {r} <= {linear_rhs}", + h = hypercube.into_iter().format(" & "), + r = linear_terms + .into_iter() + .format_with(" ", |elt, f| f(&format_args!( + "{} {}", + elt.scale, elt.inner + ))) + ) + .expect("failed to write proof"); + } + + /// Log an intermediate deduction. + pub fn intermediate_deduction( + &mut self, + hypercube: impl IntoIterator, + linear_terms: impl IntoIterator>, + linear_rhs: i32, + ) { + if !self.options.include_intermediate_steps { + return; + } + + let Some(writer) = self.proof_file.as_mut() else { + return; + }; + + writeln!( + writer, + "di {h} -> {r} <= {linear_rhs}", + h = hypercube.into_iter().format(" & "), + r = linear_terms + .into_iter() + .format_with(" ", |elt, f| f(&format_args!( + "{} {}", + elt.scale, elt.inner + ))) + ) + .expect("failed to write proof"); + } + + /// Log a deduction. + pub fn deduction( + &mut self, + constraint_tag: ConstraintTag, + hypercube: impl IntoIterator, + linear_terms: impl IntoIterator>, + linear_rhs: i32, + conflict_dl: usize, + backtrack_dl: usize, + ) { + let Some(writer) = self.proof_file.as_mut() else { + return; + }; + + writeln!( + writer, + "d {id} {h} -> {r} <= {linear_rhs} confl@{conflict_dl} bt={backtrack_dl}", + id = NonZero::from(constraint_tag), + h = hypercube.into_iter().format(" & "), + r = linear_terms + .into_iter() + .format_with(" ", |elt, f| f(&format_args!( + "{} {}", + elt.scale, elt.inner + ))) + ) + .expect("failed to write proof"); + } +} + +fn is_likely_a_constant(domain_id: DomainId, state: &State) -> bool { + let is_fixed = state.assignments.get_initial_lower_bound(domain_id) + == state.assignments.get_initial_upper_bound(domain_id); + + if !is_fixed { + return false; + } + + let value = state.assignments.get_initial_upper_bound(domain_id); + + match state.variable_names.get_int_name(domain_id) { + Some(name) => name.parse::().is_ok_and(|v| v == value), + None => true, + } +} diff --git a/pumpkin-crates/core/src/hypercube_linear/trail_view.rs b/pumpkin-crates/core/src/hypercube_linear/trail_view.rs new file mode 100644 index 000000000..b580e34e9 --- /dev/null +++ b/pumpkin-crates/core/src/hypercube_linear/trail_view.rs @@ -0,0 +1,147 @@ +use crate::hypercube_linear::explanation::HypercubeLinearExplanation; +use crate::predicates::Predicate; +use crate::variables::AffineView; +use crate::variables::DomainId; + +/// Abstracts all trail queries the hypercube linear resolver needs. +/// +/// This trait is implemented by [`crate::state::State`] for production use, and by +/// `FakeTrail` in tests. +pub(crate) trait TrailView { + fn trail_position(&self, predicate: Predicate) -> Option; + fn checkpoint_for_predicate(&self, predicate: Predicate) -> Option; + fn current_checkpoint(&self) -> usize; + fn trail_position_at_checkpoint(&self, checkpoint: usize) -> usize; + /// Returns the predicate that is directly recorded at `trail_position` on the trail. + /// + /// Used by [`crate::hypercube_linear::predicate_heap::PredicateHeap`] to determine whether a + /// predicate is a direct trail entry or an implied predicate. + fn predicate_at_trail_position(&self, trail_position: usize) -> Predicate; + fn truth_value_at(&self, predicate: Predicate, trail_position: usize) -> Option; + fn lower_bound_at_trail_position(&self, domain: DomainId, trail_position: usize) -> i32; + fn upper_bound_at_trail_position(&self, domain: DomainId, trail_position: usize) -> i32; + /// Returns the explanation for why `predicate` was propagated. + /// + /// Panics if the predicate is not propagated (i.e. is a decision or not on the trail). + fn reason_for(&mut self, predicate: Predicate) -> HypercubeLinearExplanation; + /// Returns the trail position of the last entry on the trail. + /// + /// Only valid during conflict analysis, where the trail is guaranteed to be non-empty. + fn current_trail_position(&self) -> usize; +} + +/// Computes the lower bound of an [`AffineView`] at the given trail position. +/// +/// Mirrors [`crate::variables::AffineView::lower_bound_at_trail_position`]. +pub(super) fn affine_lower_bound_at( + trail: &T, + term: AffineView, + trail_position: usize, +) -> i32 { + let domain_bound = if term.scale < 0 { + trail.upper_bound_at_trail_position(term.inner, trail_position) + } else { + trail.lower_bound_at_trail_position(term.inner, trail_position) + }; + term.scale * domain_bound + term.offset +} + +/// Computes the upper bound of an [`AffineView`] at the given trail position. +/// +/// Mirrors [`crate::variables::AffineView::upper_bound_at_trail_position`]. +pub(super) fn affine_upper_bound_at( + trail: &T, + term: AffineView, + trail_position: usize, +) -> i32 { + let domain_bound = if term.scale < 0 { + trail.lower_bound_at_trail_position(term.inner, trail_position) + } else { + trail.upper_bound_at_trail_position(term.inner, trail_position) + }; + term.scale * domain_bound + term.offset +} + +// ======== impl TrailView for State ======== + +use crate::hypercube_linear::explanation::HypercubeLinear; +use crate::propagation::ExplanationContext; +use crate::state::CurrentNogood; +use crate::state::State; + +impl TrailView for State { + fn trail_position(&self, predicate: Predicate) -> Option { + self.assignments.get_trail_position(&predicate) + } + + fn checkpoint_for_predicate(&self, predicate: Predicate) -> Option { + self.assignments.get_checkpoint_for_predicate(&predicate) + } + + fn current_checkpoint(&self) -> usize { + self.assignments.get_checkpoint() + } + + fn trail_position_at_checkpoint(&self, checkpoint: usize) -> usize { + self.assignments + .get_trail_position_at_checkpoint(checkpoint) + } + + fn predicate_at_trail_position(&self, trail_position: usize) -> Predicate { + self.assignments.get_trail_entry(trail_position).predicate + } + + fn truth_value_at(&self, predicate: Predicate, trail_position: usize) -> Option { + self.assignments + .evaluate_predicate_at_trail_position(predicate, trail_position) + } + + fn lower_bound_at_trail_position(&self, domain: DomainId, trail_position: usize) -> i32 { + self.assignments + .get_lower_bound_at_trail_position(domain, trail_position) + } + + fn upper_bound_at_trail_position(&self, domain: DomainId, trail_position: usize) -> i32 { + self.assignments + .get_upper_bound_at_trail_position(domain, trail_position) + } + + fn current_trail_position(&self) -> usize { + self.trail_len() - 1 + } + + fn reason_for(&mut self, pivot: Predicate) -> HypercubeLinearExplanation { + let trail_position = self + .assignments + .get_trail_position(&pivot) + .expect("pivot must be on trail"); + + let reason_ref = self + .assignments + .get_trail_entry(trail_position) + .reason + .expect("pivot is propagated"); + + if let Some(code) = self.reason_store.get_lazy_code(reason_ref) { + let propagator_id = self.reason_store.get_propagator(reason_ref); + + if let Some((hypercube, linear, _)) = self.propagators[propagator_id] + .explain_as_hypercube_linear( + code, + ExplanationContext::without_working_nogood( + &self.assignments, + trail_position, + &mut self.notification_engine, + ), + ) + { + return HypercubeLinearExplanation::Proper(HypercubeLinear { hypercube, linear }); + } + } + + let mut clause = vec![]; + let _ = self.get_propagation_reason(pivot, &mut clause, CurrentNogood::empty()); + clause.push(!pivot); + HypercubeLinearExplanation::Conjunction(clause) + } +} diff --git a/pumpkin-crates/core/src/lib.rs b/pumpkin-crates/core/src/lib.rs index 2a8680544..c2c3359e0 100644 --- a/pumpkin-crates/core/src/lib.rs +++ b/pumpkin-crates/core/src/lib.rs @@ -14,6 +14,7 @@ use crate::termination::TerminationCondition; pub mod branching; pub mod conflict_resolving; pub mod constraints; +pub mod hypercube_linear; pub mod optimisation; pub mod proof; pub mod propagation; diff --git a/pumpkin-crates/core/src/proof/finalizer.rs b/pumpkin-crates/core/src/proof/finalizer.rs index 97cef32c0..6a4592eba 100644 --- a/pumpkin-crates/core/src/proof/finalizer.rs +++ b/pumpkin-crates/core/src/proof/finalizer.rs @@ -102,7 +102,7 @@ fn finalize_proof_impl( // There must be some combination of other factors. let mut reason = vec![]; - ConflictAnalysisContext::get_propagation_reason_inner( + let _ = ConflictAnalysisContext::get_propagation_reason_inner( predicate, CurrentNogood::empty(), context.proof_log, @@ -157,5 +157,21 @@ pub(crate) fn explain_root_assignment( [predicate].into_iter().collect(), )]); - let _ = finalize_proof_impl(context, to_explain); + let required_assumptions = finalize_proof_impl(context, to_explain); + + // This can happen when solving under assumptions. However, we do not have a good way to + // handle assumptions in the proof format yet. So, we introduce them as initial domains. + // This would never pass an external checker, but will satisfy the runtime deduction + // verification. + for assumption in required_assumptions { + let _ = context + .proof_log + .log_domain_inference( + assumption, + &context.state.variable_names, + &mut context.state.constraint_tags, + &context.state.assignments, + ) + .expect("failed to write to proof"); + } } diff --git a/pumpkin-crates/core/src/proof/mod.rs b/pumpkin-crates/core/src/proof/mod.rs index e51ab37f7..082f97e63 100644 --- a/pumpkin-crates/core/src/proof/mod.rs +++ b/pumpkin-crates/core/src/proof/mod.rs @@ -20,6 +20,9 @@ use drcp_format::writer::ProofWriter; pub(crate) use finalizer::*; pub use inference_code::*; use proof_atomics::ProofAtomics; +use pumpkin_checking::InvalidDeduction; +use pumpkin_checking::SupportingInference; +use pumpkin_checking::verify_deduction; #[cfg(doc)] use crate::Solver; @@ -39,6 +42,7 @@ use crate::variables::Literal; #[derive(Debug, Default)] pub struct ProofLog { internal_proof: Option, + supporting_inferences: Vec>, } impl ProofLog { @@ -64,6 +68,7 @@ impl ProofLog { logged_domain_inferences: HashMap::default(), proof_atomics: ProofAtomics::default(), }), + supporting_inferences: vec![], }) } @@ -72,6 +77,7 @@ impl ProofLog { let file = File::create(file_path)?; Ok(ProofLog { internal_proof: Some(ProofImpl::DimacsProof(DimacsProof::new(file))), + supporting_inferences: vec![], }) } @@ -80,13 +86,20 @@ impl ProofLog { &mut self, constraint_tags: &mut KeyGenerator, inference_code: InferenceCode, - premises: impl IntoIterator, + premises: impl IntoIterator + Clone, propagated: Option, variable_names: &VariableNames, assignments: &Assignments, ) -> std::io::Result { let inference_tag = constraint_tags.next_key(); + if cfg!(feature = "check-deductions") { + self.supporting_inferences.push(SupportingInference { + premises: premises.clone().into_iter().collect(), + consequent: propagated, + }); + } + let Some(ProofImpl::CpProof { writer, propagation_order_hint: Some(propagation_sequence), @@ -126,7 +139,12 @@ impl ProofLog { constraint_tags: &mut KeyGenerator, assignments: &Assignments, ) -> std::io::Result> { - assert!(assignments.is_initial_bound(predicate)); + if cfg!(feature = "check-deductions") { + self.supporting_inferences.push(SupportingInference { + premises: vec![], + consequent: Some(predicate), + }); + } if is_likely_a_constant(predicate, variable_names, assignments) { // The predicate is over a constant variable. We assume we do not want to @@ -184,13 +202,17 @@ impl ProofLog { /// order. pub(crate) fn log_deduction( &mut self, - premises: impl IntoIterator, + premises: impl IntoIterator + Clone, variable_names: &VariableNames, constraint_tags: &mut KeyGenerator, assignments: &Assignments, ) -> std::io::Result { let constraint_tag = constraint_tags.next_key(); + if cfg!(feature = "check-deductions") { + self.verify_deduction_at_runtime(premises.clone()); + } + match &mut self.internal_proof { Some(ProofImpl::CpProof { writer, @@ -286,7 +308,7 @@ impl ProofLog { propagation_order_hint: Some(_), .. }) - ) + ) || cfg!(feature = "check-deductions") } pub(crate) fn reify_predicate(&mut self, literal: Literal, predicate: Predicate) { @@ -304,6 +326,42 @@ impl ProofLog { pub(crate) fn is_logging_proof(&self) -> bool { self.internal_proof.is_some() } + + fn verify_deduction_at_runtime( + &mut self, + premises: impl IntoIterator + Clone, + ) { + match verify_deduction( + premises.clone(), + self.supporting_inferences.iter().cloned().rev(), + ) { + Ok(_) => { + self.supporting_inferences.clear(); + } + Err(InvalidDeduction(ignored_inferences)) => { + eprintln!("Supporting inferences:"); + for inference in self.supporting_inferences.iter() { + eprintln!("{:?} -> {:?}", inference.premises, inference.consequent); + } + + if !ignored_inferences.is_empty() { + eprintln!("Ignored inferences:"); + for ignored_inference in ignored_inferences { + eprintln!( + "{:?} -> {:?}", + ignored_inference.inference.premises, + ignored_inference.inference.consequent + ); + } + } + + panic!( + "Failed to verify deduction: {:?} -> false", + itertools::join(premises, " & ") + ); + } + } + } } /// Returns `true` if the given predicate is likely a constant from the model that was unnamed. diff --git a/pumpkin-crates/core/src/propagation/contexts/explanation_context.rs b/pumpkin-crates/core/src/propagation/contexts/explanation_context.rs index 1158a6887..33ceb3c69 100644 --- a/pumpkin-crates/core/src/propagation/contexts/explanation_context.rs +++ b/pumpkin-crates/core/src/propagation/contexts/explanation_context.rs @@ -63,6 +63,19 @@ impl<'a> ExplanationContext<'a> { } } + pub fn reborrow(&mut self) -> ExplanationContext<'_> { + ExplanationContext { + assignments: self.assignments, + notification_engine: self.notification_engine, + current_nogood: CurrentNogood { + heap: &self.current_nogood.heap, + visited: &self.current_nogood.visited, + ids: &self.current_nogood.ids, + }, + trail_position: self.trail_position, + } + } + pub fn get_predicate(&mut self, predicate_id: PredicateId) -> Predicate { self.notification_engine.get_predicate(predicate_id) } diff --git a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs index 5c9d67a48..1e6e42d30 100644 --- a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs +++ b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs @@ -1,3 +1,5 @@ +use log::trace; + use crate::basic_types::PredicateId; use crate::engine::Assignments; use crate::engine::EmptyDomain; @@ -10,7 +12,6 @@ use crate::engine::reason::Reason; use crate::engine::reason::ReasonStore; use crate::engine::reason::StoredReason; use crate::engine::variables::Literal; -use crate::proof::InferenceCode; use crate::propagation::DomainEvents; use crate::propagation::Domains; use crate::propagation::HasAssignments; @@ -134,8 +135,11 @@ impl<'a> PropagationContext<'a> { /// Stop being enqueued for the given predicate. pub fn unregister_predicate(&mut self, predicate_id: PredicateId) { - self.notification_engine - .unwatch_predicate(predicate_id, self.propagator_id); + self.notification_engine.unwatch_predicate( + predicate_id, + self.propagator_id, + self.assignments, + ); } /// Subscribes the propagator to the given [`DomainEvents`]. @@ -240,16 +244,23 @@ impl PropagationContext<'_> { &mut self, predicate: Predicate, reason: impl Into, - inference_code: &InferenceCode, ) -> Result<(), EmptyDomainConflict> { let slot = self.reason_store.new_slot(); let modification_result = self.assignments.post_predicate( predicate, - Some((slot.reason_ref(), inference_code.clone())), + Some(slot.reason_ref()), self.notification_engine, ); + if !matches!(modification_result, Ok(false)) { + trace!( + "propagated {predicate} @ {dl} by {propagator:?} with result {modification_result:?}", + dl = self.assignments.get_checkpoint(), + propagator = self.propagator_id, + ); + } + match modification_result { Ok(false) => Ok(()), Ok(true) => { @@ -264,13 +275,12 @@ impl PropagationContext<'_> { self.propagator_id, build_reason(reason, self.reification_literal), ); - let (trigger_predicate, trigger_reason, trigger_inference_code) = + let (trigger_predicate, trigger_reason) = self.assignments.remove_last_trail_element(); Err(EmptyDomainConflict { trigger_predicate, trigger_reason: Some(trigger_reason), - trigger_inference_code: Some(trigger_inference_code), }) } } @@ -282,13 +292,13 @@ pub(crate) fn build_reason( reification_literal: Option, ) -> StoredReason { match reason.into() { - Reason::Eager(mut conjunction) => { + Reason::Eager(mut conjunction, inference_code) => { conjunction.extend( reification_literal .iter() .map(|lit| lit.get_true_predicate()), ); - StoredReason::Eager(conjunction) + StoredReason::Eager(conjunction, inference_code) } Reason::DynamicLazy(code) => StoredReason::DynamicLazy(code), } diff --git a/pumpkin-crates/core/src/propagation/domains.rs b/pumpkin-crates/core/src/propagation/domains.rs index c79f0aed5..09d820477 100644 --- a/pumpkin-crates/core/src/propagation/domains.rs +++ b/pumpkin-crates/core/src/propagation/domains.rs @@ -58,6 +58,14 @@ pub trait ReadDomains { /// currently unassigned. fn evaluate_predicate(&self, predicate: Predicate) -> Option; + /// Returns whether the provided [`Predicate`] is assigned (either true or false) or is + /// currently unassigned at the given trail position. + fn evaluate_predicate_at_trail_position( + &self, + predicate: Predicate, + trail_position: usize, + ) -> Option; + /// Returns whether the provided [`Literal`] is assigned (either true or false) or is /// currently unassigned. fn evaluate_literal(&self, literal: Literal) -> Option; @@ -166,6 +174,15 @@ impl ReadDomains for T { self.assignments().evaluate_predicate(predicate) } + fn evaluate_predicate_at_trail_position( + &self, + predicate: Predicate, + trail_position: usize, + ) -> Option { + self.assignments() + .evaluate_predicate_at_trail_position(predicate, trail_position) + } + fn evaluate_literal(&self, literal: Literal) -> Option { self.evaluate_predicate(literal.get_true_predicate()) } diff --git a/pumpkin-crates/core/src/propagation/propagator.rs b/pumpkin-crates/core/src/propagation/propagator.rs index 720081340..8e581caa6 100644 --- a/pumpkin-crates/core/src/propagation/propagator.rs +++ b/pumpkin-crates/core/src/propagation/propagator.rs @@ -15,7 +15,10 @@ use crate::engine::ConstraintSatisfactionSolver; use crate::engine::PropagationStatusCP; use crate::engine::PropagatorConflict; use crate::engine::notifications::OpaqueDomainEvent; +use crate::hypercube_linear::Hypercube; +use crate::hypercube_linear::LinearInequality; use crate::predicates::Predicate; +use crate::proof::InferenceCode; #[cfg(doc)] use crate::propagation::DomainEvent; #[cfg(doc)] @@ -33,6 +36,16 @@ use crate::pumpkin_asserts::PUMPKIN_ASSERT_EXTREME; use crate::state::Conflict; use crate::statistics::statistic_logger::StatisticLogger; +/// The result of [`Propagator::lazy_explanation`]: a slice of predicates that form the reason for +/// a propagation, together with the [`InferenceCode`] that identifies the explanation algorithm. +#[derive(Clone, Debug)] +pub struct LazyExplanation<'a> { + /// The predicates that explain the propagation. + pub predicates: &'a [Predicate], + /// The inference code identifying the explanation algorithm. + pub inference_code: InferenceCode, +} + // We need to use this to cast from `Box` to `NogoodPropagator`; rust inherently // does not allow downcasting from the trait definition to its concrete type. impl_downcast!(Propagator); @@ -195,7 +208,11 @@ pub trait Propagator: Downcast + DynClone { /// explanation is generated); the bounds at the time of the propagation can be retrieved using /// methods such as [`ReadDomains::lower_bound_at_trail_position`] in combination /// with [`ExplanationContext::get_trail_position`]. - fn lazy_explanation(&mut self, _code: u64, _context: ExplanationContext) -> &[Predicate] { + fn lazy_explanation( + &mut self, + _code: u64, + _context: ExplanationContext, + ) -> LazyExplanation<'_> { panic!( "{}", format!( @@ -205,6 +222,14 @@ pub trait Propagator: Downcast + DynClone { ); } + fn explain_as_hypercube_linear( + &mut self, + _code: u64, + _context: ExplanationContext, + ) -> Option<(Hypercube, LinearInequality, InferenceCode)> { + None + } + /// Logs statistics of the propagator using the provided [`StatisticLogger`]. /// /// It is recommended to create a struct through the [`create_statistics_struct!`] macro! diff --git a/pumpkin-crates/core/src/propagators/hypercube_linear/hypercube.rs b/pumpkin-crates/core/src/propagators/hypercube_linear/hypercube.rs deleted file mode 100644 index 0a8caf3a3..000000000 --- a/pumpkin-crates/core/src/propagators/hypercube_linear/hypercube.rs +++ /dev/null @@ -1,132 +0,0 @@ -use pumpkin_checking::IntExt; -use pumpkin_checking::VariableState; - -use crate::predicate; -use crate::predicates::Predicate; -use crate::variables::DomainId; - -/// Error that occurs when constructing a [`Hypercube`]. -/// -/// If the domain of a variable becomes empty, the hypercube is inconsistent and cannot be -/// constructed. -#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)] -#[error("domain {0} is empty in the hypercube")] -pub struct InconsistentHypercube(DomainId); - -/// A region in the solution space. -/// -/// The hypercube will always be consistent. -#[derive(Clone, Debug)] -pub struct Hypercube { - state: VariableState, -} - -impl Hypercube { - /// Create a new hypercube from a sequence of predicates. - /// - /// If the predicates are inconsistent, the [`Err`] variant is returned. - pub fn new( - predicates: impl IntoIterator, - ) -> Result { - // Note: Ideally this would be an implementation of [`TryFrom`], however, that cannot be - // done in the same way due to a 'conflicting implementations' error. - - let state = VariableState::prepare_for_conflict_check(predicates, None) - .map_err(InconsistentHypercube)?; - - Ok(Hypercube { state }) - } - - /// Get all predicates that define the hypercube. - pub fn iter_predicates(&self) -> impl Iterator + '_ { - self.state.domains().flat_map(|domain_id| { - let lower_bound_predicate = - if let IntExt::Int(lower_bound) = self.state.lower_bound(domain_id) { - Some(predicate![domain_id >= lower_bound]) - } else { - None - }; - let upper_bound_predicate = - if let IntExt::Int(upper_bound) = self.state.upper_bound(domain_id) { - Some(predicate![domain_id <= upper_bound]) - } else { - None - }; - - [lower_bound_predicate, upper_bound_predicate] - .into_iter() - .flatten() - .chain( - self.state - .holes(domain_id) - .map(|value| predicate![domain_id != value]), - ) - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::containers::HashSet; - use crate::predicate; - use crate::state::State; - - #[test] - fn consistent_hypercube_can_be_created() { - let mut state = State::default(); - - let x = state.new_interval_variable(1, 10, Some("x".into())); - let y = state.new_interval_variable(1, 10, Some("y".into())); - - let maybe_hypercube = Hypercube::new([predicate![x >= 2], predicate![y >= 2]]); - - assert!(maybe_hypercube.is_ok()); - } - - #[test] - fn inconsistent_hypercube_can_be_created() { - let mut state = State::default(); - - let x = state.new_interval_variable(1, 10, Some("x".into())); - let y = state.new_interval_variable(1, 10, Some("y".into())); - - let error = Hypercube::new([predicate![x >= 2], predicate![y >= 2], predicate![x <= 1]]) - .expect_err("hypercube is inconsistent"); - - assert_eq!(InconsistentHypercube(x), error); - } - - #[test] - fn hypercube_iters_predicates_from_constructor() { - let mut state = State::default(); - - let x = state.new_interval_variable(1, 10, Some("x".into())); - let y = state.new_interval_variable(1, 10, Some("y".into())); - - let hypercube = - Hypercube::new([predicate![x >= 2], predicate![y >= 2]]).expect("not inconsistent"); - - assert_eq!( - [predicate![x >= 2], predicate![y >= 2]] - .into_iter() - .collect::>(), - hypercube.iter_predicates().collect::>(), - ); - } - - #[test] - fn iterating_predicates_ignores_subsumed_predicates() { - let mut state = State::default(); - - let x = state.new_interval_variable(1, 10, Some("x".into())); - - let hypercube = - Hypercube::new([predicate![x >= 2], predicate![x >= 4]]).expect("not inconsistent"); - - assert_eq!( - [predicate![x >= 4]].into_iter().collect::>(), - hypercube.iter_predicates().collect::>(), - ); - } -} diff --git a/pumpkin-crates/core/src/propagators/hypercube_linear/mod.rs b/pumpkin-crates/core/src/propagators/hypercube_linear/mod.rs deleted file mode 100644 index 13e6e1345..000000000 --- a/pumpkin-crates/core/src/propagators/hypercube_linear/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod checker; -mod hypercube; -mod linear; -mod propagator; - -pub use checker::*; -pub use hypercube::*; -pub use linear::*; -pub use propagator::*; diff --git a/pumpkin-crates/core/src/propagators/mod.rs b/pumpkin-crates/core/src/propagators/mod.rs index 2af1b61a5..2d88c10e4 100644 --- a/pumpkin-crates/core/src/propagators/mod.rs +++ b/pumpkin-crates/core/src/propagators/mod.rs @@ -1,4 +1,3 @@ -pub mod hypercube_linear; pub mod nogoods; pub(crate) mod reified_propagator; diff --git a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs index b89a6a413..8da3d527b 100644 --- a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs +++ b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs @@ -22,6 +22,7 @@ use crate::predicate; use crate::proof::InferenceCode; use crate::propagation::EnqueueDecision; use crate::propagation::ExplanationContext; +use crate::propagation::LazyExplanation; use crate::propagation::NotificationContext; use crate::propagation::Priority; use crate::propagation::PropagationContext; @@ -176,7 +177,7 @@ impl NogoodPropagator { .get_trail_position(&!notification_engine.get_predicate(nogood[0])) .unwrap(); let trail_entry = assignments.get_trail_entry(trail_position); - if let Some((reason_ref, _)) = trail_entry.reason { + if let Some(reason_ref) = trail_entry.reason { let propagator_id = reason_store.get_propagator(reason_ref); let code = reason_store.get_lazy_code(reason_ref); @@ -184,7 +185,7 @@ impl NogoodPropagator { let propagated_by_nogood_propagator = propagator_id == handle.propagator_id(); // Then we check whether the lazy reason for the propagation was this particular // nogood - let code_matches_id = code.is_none() || *code.unwrap() == id.id as u64; + let code_matches_id = code.is_none() || code.unwrap() == id.id as u64; return propagated_by_nogood_propagator && code_matches_id; } } @@ -317,12 +318,7 @@ impl Propagator for NogoodPropagator { let reason = Reason::DynamicLazy(watcher.nogood_id.id as u64); let predicate = !context.get_predicate(nogood_predicates[0]); - let result = context.post( - predicate, - reason, - &self.inference_codes - [self.nogood_predicates.get_nogood_index(&watcher.nogood_id)], - ); + let result = context.post(predicate, reason); // If the propagation lead to a conflict. if let Err(e) = result { return Err(e.into()); @@ -357,7 +353,11 @@ impl Propagator for NogoodPropagator { /// /// In case of the noogood propagator, lazy explanations internally also update information /// about the LBD and activity of the nogood, which is used when cleaning up nogoods. - fn lazy_explanation(&mut self, code: u64, mut context: ExplanationContext) -> &[Predicate] { + fn lazy_explanation( + &mut self, + code: u64, + mut context: ExplanationContext, + ) -> LazyExplanation<'_> { let id = NogoodId { id: code as u32 }; self.temp_nogood_reason = self.nogood_predicates[id][1..] @@ -415,8 +415,10 @@ impl Propagator for NogoodPropagator { // At this point, it is safe to increase the activity value self.nogood_info[info_id].activity += self.parameters.activity_bump_increment; } - // update LBD, so we need code plus assignments as input. - &self.temp_nogood_reason + LazyExplanation { + predicates: self.temp_nogood_reason.as_slice(), + inference_code: self.inference_codes[info_id].clone(), + } } } @@ -486,14 +488,12 @@ impl NogoodPropagator { // Then we propagate the asserting predicate and as the reason we give the index to the // asserting nogood such that we can re-create the reason when asked for it let reason = Reason::DynamicLazy(nogood_id.id as u64); - let inference_code = - &self.inference_codes[self.nogood_predicates.get_nogood_index(&nogood_id)]; let predicate = !context .notification_engine .get_predicate(self.nogood_predicates[nogood_id][0]); context - .post(predicate, reason, inference_code) + .post(predicate, reason) .expect("Cannot fail to add the asserting predicate."); // We then assign the nogood to the correct tier based on its LBD @@ -608,8 +608,10 @@ impl NogoodPropagator { // Post the negated predicate at the root to respect the nogood. context.post( !nogood[0], - PropositionalConjunction::from(input_nogood), - &inference_code, + ( + PropositionalConjunction::from(input_nogood), + &inference_code, + ), )?; Ok(()) } @@ -1234,7 +1236,7 @@ impl NogoodPropagator { .filter(|&p| p != !propagated_predicate) .collect::(); - context.post(propagated_predicate, reason, inference_code)?; + context.post(propagated_predicate, (reason, inference_code))?; } Ok(()) } diff --git a/pumpkin-crates/core/src/propagators/reified_propagator.rs b/pumpkin-crates/core/src/propagators/reified_propagator.rs index 1b8609363..0bb782495 100644 --- a/pumpkin-crates/core/src/propagators/reified_propagator.rs +++ b/pumpkin-crates/core/src/propagators/reified_propagator.rs @@ -11,6 +11,7 @@ use crate::propagation::Domains; use crate::propagation::EnqueueDecision; use crate::propagation::ExplanationContext; use crate::propagation::InferenceCheckers; +use crate::propagation::LazyExplanation; use crate::propagation::LocalId; use crate::propagation::NotificationContext; use crate::propagation::Priority; @@ -155,13 +156,19 @@ impl Propagator for ReifiedPropagator &[Predicate] { + fn lazy_explanation(&mut self, code: u64, context: ExplanationContext) -> LazyExplanation<'_> { + let inner = self.propagator.lazy_explanation(code, context); + let inference_code = inner.inference_code; + self.reason_buffer.clear(); self.reason_buffer .push(self.reification_literal.get_true_predicate()); - self.reason_buffer - .extend(self.propagator.lazy_explanation(code, context)); - &self.reason_buffer + self.reason_buffer.extend(inner.predicates); + + LazyExplanation { + predicates: self.reason_buffer.as_slice(), + inference_code, + } } } @@ -186,8 +193,7 @@ impl ReifiedPropagator { if let Some(conflict) = self.propagator.detect_inconsistency(context.domains()) { context.post( self.reification_literal.get_false_predicate(), - conflict.conjunction, - &conflict.inference_code, + (conflict.conjunction, &conflict.inference_code), )?; } @@ -327,8 +333,10 @@ mod tests { move |mut ctx: PropagationContext| { ctx.post( predicate![var >= 3], - conjunction!(), - &InferenceCode::unknown_label(ConstraintTag::create_from_index(0)), + ( + conjunction!(), + &InferenceCode::unknown_label(ConstraintTag::create_from_index(0)), + ), )?; Ok(()) }, diff --git a/pumpkin-crates/propagators/Cargo.toml b/pumpkin-crates/propagators/Cargo.toml index 02e9a4e60..7b3e7f0d4 100644 --- a/pumpkin-crates/propagators/Cargo.toml +++ b/pumpkin-crates/propagators/Cargo.toml @@ -14,8 +14,8 @@ workspace = true pumpkin-core = { version = "0.3.0", path = "../core" } pumpkin-checking = { version = "0.3.0", path = "../checking" } enumset = "1.1.2" -bitfield-struct = "0.9.2" -convert_case = "0.8.0" +bitfield-struct = "0.13.0" +convert_case = "0.11.0" clap = { version = "4.5.40", optional = true, features=["derive"]} [features] diff --git a/pumpkin-crates/propagators/clippy.toml b/pumpkin-crates/propagators/clippy.toml index 5ac8d8e6a..6677c5e9d 100644 --- a/pumpkin-crates/propagators/clippy.toml +++ b/pumpkin-crates/propagators/clippy.toml @@ -1,7 +1,11 @@ disallowed-types = [ { path = "std::collections::HashSet", reason = "use pumpkin_solver::core::containers::HashSet" }, { path = "std::collections::HashMap", reason = "use pumpkin_solver::core::containers::HashMap" }, - { path = "rand::RngCore", reason = "use pumpkin_solver::basic_types::Random" }, { path = "rand::Rng", reason = "use pumpkin_solver::basic_types::Random" }, + { path = "rand::RngExt", reason = "use pumpkin_solver::basic_types::Random" }, { path = "rand::SeedableRng", reason = "use pumpkin_solver::basic_types::Random" }, ] + +allowed-duplicate-crates = [ + "wit-bindgen", +] diff --git a/pumpkin-crates/propagators/src/lib.rs b/pumpkin-crates/propagators/src/lib.rs index b78a60169..6210ef73b 100644 --- a/pumpkin-crates/propagators/src/lib.rs +++ b/pumpkin-crates/propagators/src/lib.rs @@ -4,3 +4,29 @@ //! [`pumpkin_core::propagation`]. mod propagators; pub use propagators::*; +#[cfg(test)] +use pumpkin_core::state::State; +#[cfg(test)] +use pumpkin_core::variables::DomainId; + +/// Utilities that simplify test code using the [`State`]. +#[cfg(test)] +pub(crate) trait StateExt { + /// Assert that the bounds of the given `domain_id` match the provided `lower_bound` and + /// `upper_bound`. + fn assert_bounds(&self, domain_id: DomainId, lower_bound: i32, upper_bound: i32); +} + +#[cfg(test)] +impl StateExt for State { + fn assert_bounds(&self, domain_id: DomainId, lower_bound: i32, upper_bound: i32) { + let actual_lb = self.lower_bound(domain_id); + let actual_ub = self.upper_bound(domain_id); + + assert_eq!( + (lower_bound, upper_bound), + (actual_lb, actual_ub), + "The expected bounds [{lower_bound}..{upper_bound}] did not match the actual bounds [{actual_lb}..{actual_ub}]" + ); + } +} diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs index 82e7a51c9..2881dfa39 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs @@ -94,8 +94,7 @@ where // zero at the root. context.post( predicate![self.absolute >= 0], - conjunction!(), - &self.inference_code, + (conjunction!(), &self.inference_code), )?; // Propagating absolute value can be broken into a few cases: @@ -112,21 +111,27 @@ where context.post( predicate![self.absolute <= signed_absolute_ub], - conjunction!([self.signed >= signed_lb] & [self.signed <= signed_ub]), - &self.inference_code, + ( + conjunction!([self.signed >= signed_lb] & [self.signed <= signed_ub]), + &self.inference_code, + ), )?; if signed_lb > 0 { context.post( predicate![self.absolute >= signed_lb], - conjunction!([self.signed >= signed_lb]), - &self.inference_code, + ( + conjunction!([self.signed >= signed_lb]), + &self.inference_code, + ), )?; } else if signed_ub < 0 { context.post( predicate![self.absolute >= signed_ub.abs()], - conjunction!([self.signed <= signed_ub]), - &self.inference_code, + ( + conjunction!([self.signed <= signed_ub]), + &self.inference_code, + ), )?; } @@ -134,26 +139,34 @@ where let absolute_lb = context.lower_bound(&self.absolute); context.post( predicate![self.signed >= -absolute_ub], - conjunction!([self.absolute <= absolute_ub]), - &self.inference_code, + ( + conjunction!([self.absolute <= absolute_ub]), + &self.inference_code, + ), )?; context.post( predicate![self.signed <= absolute_ub], - conjunction!([self.absolute <= absolute_ub]), - &self.inference_code, + ( + conjunction!([self.absolute <= absolute_ub]), + &self.inference_code, + ), )?; if signed_ub <= 0 { context.post( predicate![self.signed <= -absolute_lb], - conjunction!([self.signed <= 0] & [self.absolute >= absolute_lb]), - &self.inference_code, + ( + conjunction!([self.signed <= 0] & [self.absolute >= absolute_lb]), + &self.inference_code, + ), )?; } else if signed_lb >= 0 { context.post( predicate![self.signed >= absolute_lb], - conjunction!([self.signed >= 0] & [self.absolute >= absolute_lb]), - &self.inference_code, + ( + conjunction!([self.signed >= 0] & [self.absolute >= absolute_lb]), + &self.inference_code, + ), )?; } @@ -205,124 +218,118 @@ where } } -#[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { - use pumpkin_core::TestSolver; + use pumpkin_core::state::State; use super::*; + use crate::StateExt; #[test] fn absolute_bounds_are_propagated_at_initialise() { - let mut solver = TestSolver::default(); + let mut state = State::default(); - let signed = solver.new_variable(-3, 4); - let absolute = solver.new_variable(-2, 10); - let constraint_tag = solver.new_constraint_tag(); + let signed = state.new_interval_variable(-3, 4, None); + let absolute = state.new_interval_variable(-2, 10, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(AbsoluteValueArgs { - signed, - absolute, - constraint_tag, - }) - .expect("no empty domains"); + let _ = state.add_propagator(AbsoluteValueArgs { + signed, + absolute, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domains"); - solver.assert_bounds(absolute, 0, 4); + state.assert_bounds(absolute, 0, 4); } #[test] fn signed_bounds_are_propagated_at_initialise() { - let mut solver = TestSolver::default(); + let mut state = State::default(); - let signed = solver.new_variable(-5, 5); - let absolute = solver.new_variable(0, 3); - let constraint_tag = solver.new_constraint_tag(); + let signed = state.new_interval_variable(-5, 5, None); + let absolute = state.new_interval_variable(0, 3, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(AbsoluteValueArgs { - signed, - absolute, - constraint_tag, - }) - .expect("no empty domains"); + let _ = state.add_propagator(AbsoluteValueArgs { + signed, + absolute, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domains"); - solver.assert_bounds(signed, -3, 3); + state.assert_bounds(signed, -3, 3); } #[test] fn absolute_lower_bound_can_be_strictly_positive() { - let mut solver = TestSolver::default(); + let mut state = State::default(); - let signed = solver.new_variable(3, 6); - let absolute = solver.new_variable(0, 10); - let constraint_tag = solver.new_constraint_tag(); + let signed = state.new_interval_variable(3, 6, None); + let absolute = state.new_interval_variable(0, 10, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(AbsoluteValueArgs { - signed, - absolute, - constraint_tag, - }) - .expect("no empty domains"); + let _ = state.add_propagator(AbsoluteValueArgs { + signed, + absolute, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domains"); - solver.assert_bounds(absolute, 3, 6); + state.assert_bounds(absolute, 3, 6); } #[test] fn strictly_negative_signed_value_can_propagate_lower_bound_on_absolute() { - let mut solver = TestSolver::default(); + let mut state = State::default(); - let signed = solver.new_variable(-5, -3); - let absolute = solver.new_variable(1, 5); - let constraint_tag = solver.new_constraint_tag(); + let signed = state.new_interval_variable(-5, -3, None); + let absolute = state.new_interval_variable(1, 5, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(AbsoluteValueArgs { - signed, - absolute, - constraint_tag, - }) - .expect("no empty domains"); + let _ = state.add_propagator(AbsoluteValueArgs { + signed, + absolute, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domains"); - solver.assert_bounds(absolute, 3, 5); + state.assert_bounds(absolute, 3, 5); } #[test] fn lower_bound_on_absolute_can_propagate_negative_upper_bound_on_signed() { - let mut solver = TestSolver::default(); + let mut state = State::default(); - let signed = solver.new_variable(-5, 0); - let absolute = solver.new_variable(1, 5); - let constraint_tag = solver.new_constraint_tag(); + let signed = state.new_interval_variable(-5, 0, None); + let absolute = state.new_interval_variable(1, 5, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(AbsoluteValueArgs { - signed, - absolute, - constraint_tag, - }) - .expect("no empty domains"); + let _ = state.add_propagator(AbsoluteValueArgs { + signed, + absolute, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domains"); - solver.assert_bounds(signed, -5, -1); + state.assert_bounds(signed, -5, -1); } #[test] fn lower_bound_on_absolute_can_propagate_positive_lower_bound_on_signed() { - let mut solver = TestSolver::default(); + let mut state = State::default(); - let signed = solver.new_variable(1, 5); - let absolute = solver.new_variable(3, 5); - let constraint_tag = solver.new_constraint_tag(); + let signed = state.new_interval_variable(1, 5, None); + let absolute = state.new_interval_variable(3, 5, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(AbsoluteValueArgs { - signed, - absolute, - constraint_tag, - }) - .expect("no empty domains"); + let _ = state.add_propagator(AbsoluteValueArgs { + signed, + absolute, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domains"); - solver.assert_bounds(signed, 3, 5); + state.assert_bounds(signed, 3, 5); } } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs index 54b4a7ff7..b95b86b46 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs @@ -22,6 +22,7 @@ use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; use pumpkin_core::propagation::ExplanationContext; use pumpkin_core::propagation::InferenceCheckers; +use pumpkin_core::propagation::LazyExplanation; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; use pumpkin_core::propagation::OpaqueDomainEvent; @@ -156,7 +157,6 @@ where .with_predicate_type(predicate_type) .with_value(value) .into_bits(), - &self.inference_code, ) } } @@ -305,7 +305,7 @@ where Ok(()) } - fn lazy_explanation(&mut self, code: u64, _: ExplanationContext) -> &[Predicate] { + fn lazy_explanation(&mut self, code: u64, _: ExplanationContext) -> LazyExplanation<'_> { use PredicateType::*; use Variable::*; @@ -324,7 +324,10 @@ where self.reason = explanation; - slice::from_ref(&self.reason) + LazyExplanation { + predicates: slice::from_ref(&self.reason), + inference_code: self.inference_code.clone(), + } } fn propagate_from_scratch(&self, mut context: PropagationContext) -> PropagationStatusCP { @@ -439,59 +442,60 @@ where } } -#[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { - use pumpkin_core::TestSolver; - use pumpkin_core::propagation::EnqueueDecision; + use pumpkin_core::state::State; + use crate::StateExt; use crate::propagators::arithmetic::BinaryEqualsPropagatorArgs; #[test] fn test_propagation_of_bounds() { - let mut solver = TestSolver::default(); - let a = solver.new_variable(0, 5); - let b = solver.new_variable(3, 7); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let a = state.new_interval_variable(0, 5, None); + let b = state.new_interval_variable(3, 7, None); + let constraint_tag = state.new_constraint_tag(); - let result = solver.new_propagator(BinaryEqualsPropagatorArgs { + let _ = state.add_propagator(BinaryEqualsPropagatorArgs { a, b, constraint_tag, }); + state.propagate_to_fixed_point().expect("no conflict"); - assert!(result.is_ok()); - - solver.assert_bounds(a, 3, 5); - solver.assert_bounds(b, 3, 5); + state.assert_bounds(a, 3, 5); + state.assert_bounds(b, 3, 5); } #[test] fn test_propagation_of_holes() { - let mut solver = TestSolver::default(); - let a = solver.new_sparse_variable(vec![2, 4, 6, 9]); - let b = solver.new_sparse_variable(vec![3, 4, 7, 9]); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let a = state.new_sparse_variable(vec![2, 4, 6, 9], None); + let b = state.new_sparse_variable(vec![3, 4, 7, 9], None); + let constraint_tag = state.new_constraint_tag(); - let result = solver.new_propagator(BinaryEqualsPropagatorArgs { + let _ = state.add_propagator(BinaryEqualsPropagatorArgs { a, b, constraint_tag, }); + state.propagate_to_fixed_point().expect("no conflict"); - assert!(result.is_ok()); - - solver.assert_bounds(a, 4, 9); - solver.assert_bounds(b, 4, 9); + state.assert_bounds(a, 4, 9); + state.assert_bounds(b, 4, 9); for i in 5..=8 { - assert!(!solver.contains(a, i)); - assert!(!solver.contains(b, i)); + assert!(!state.contains(a, i)); + assert!(!state.contains(b, i)); } } + #[allow(deprecated, reason = "Uses TestSolver for EnqueueDecision assertions")] #[test] fn test_propagation_of_holes_incremental() { + use pumpkin_core::TestSolver; + use pumpkin_core::propagation::EnqueueDecision; + let mut solver = TestSolver::default(); let a = solver.new_variable(2, 9); let b = solver.new_variable(3, 9); @@ -527,17 +531,18 @@ mod tests { #[test] fn test_conflict() { - let mut solver = TestSolver::default(); - let a = solver.new_variable(0, 5); - let b = solver.new_variable(6, 9); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let a = state.new_interval_variable(0, 5, None); + let b = state.new_interval_variable(6, 9, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(BinaryEqualsPropagatorArgs { - a, - b, - constraint_tag, - }) - .expect_err("Expected result to be err"); + let _ = state.add_propagator(BinaryEqualsPropagatorArgs { + a, + b, + constraint_tag, + }); + let _ = state + .propagate_to_fixed_point() + .expect_err("expected conflict"); } } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs index 7e303f4cf..b33a98543 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs @@ -126,8 +126,7 @@ where if a_lb == a_ub { context.post( predicate!(self.b != a_lb), - conjunction!([self.a == a_lb]), - &self.inference_code, + (conjunction!([self.a == a_lb]), &self.inference_code), )?; } @@ -135,8 +134,7 @@ where if b_lb == b_ub { context.post( predicate!(self.a != b_lb), - conjunction!([self.b == b_lb]), - &self.inference_code, + (conjunction!([self.b == b_lb]), &self.inference_code), )?; } @@ -161,16 +159,14 @@ where if a_lb == a_ub { context.post( predicate!(self.b != a_lb), - conjunction!([self.a == a_lb]), - &self.inference_code, + (conjunction!([self.a == a_lb]), &self.inference_code), )?; } if b_lb == b_ub { context.post( predicate!(self.a != b_lb), - conjunction!([self.b == b_lb]), - &self.inference_code, + (conjunction!([self.b == b_lb]), &self.inference_code), )?; } @@ -202,50 +198,55 @@ where } } -#[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { - use pumpkin_core::TestSolver; - use pumpkin_core::propagation::EnqueueDecision; + use pumpkin_core::state::State; + use crate::StateExt; use crate::propagators::arithmetic::BinaryNotEqualsPropagatorArgs; #[test] fn detects_conflict() { - let mut solver = TestSolver::default(); - let a = solver.new_variable(0, 0); - let b = solver.new_variable(0, 0); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let a = state.new_interval_variable(0, 0, None); + let b = state.new_interval_variable(0, 0, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(BinaryNotEqualsPropagatorArgs { - a, - b, - constraint_tag, - }) + let _ = state.add_propagator(BinaryNotEqualsPropagatorArgs { + a, + b, + constraint_tag, + }); + let _ = state + .propagate_to_fixed_point() .expect_err("Expected conflict to be detected"); } #[test] fn propagate_when_one_is_fixed() { - let mut solver = TestSolver::default(); - let a = solver.new_variable(0, 0); - let b = solver.new_variable(0, 1); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let a = state.new_interval_variable(0, 0, None); + let b = state.new_interval_variable(0, 1, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(BinaryNotEqualsPropagatorArgs { - a, - b, - constraint_tag, - }) + let _ = state.add_propagator(BinaryNotEqualsPropagatorArgs { + a, + b, + constraint_tag, + }); + state + .propagate_to_fixed_point() .expect("Expected no conflict to be detected"); - solver.assert_bounds(b, 1, 1); + state.assert_bounds(b, 1, 1); } + #[allow(deprecated, reason = "Uses TestSolver for EnqueueDecision assertions")] #[test] fn incremental_propagation() { + use pumpkin_core::TestSolver; + use pumpkin_core::propagation::EnqueueDecision; + let mut solver = TestSolver::default(); let a = solver.new_variable(0, 0); let b = solver.new_variable(0, 10); @@ -273,20 +274,21 @@ mod tests { #[test] fn non_overlapping_is_ok() { - let mut solver = TestSolver::default(); - let a = solver.new_variable(0, 5); - let b = solver.new_variable(6, 10); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let a = state.new_interval_variable(0, 5, None); + let b = state.new_interval_variable(6, 10, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(BinaryNotEqualsPropagatorArgs { - a, - b, - constraint_tag, - }) + let _ = state.add_propagator(BinaryNotEqualsPropagatorArgs { + a, + b, + constraint_tag, + }); + state + .propagate_to_fixed_point() .expect("Expected no conflict to be detected"); - solver.assert_bounds(a, 0, 5); - solver.assert_bounds(b, 6, 10); + state.assert_bounds(a, 0, 5); + state.assert_bounds(b, 6, 10); } } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs index a1138a827..882495a48 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs @@ -224,12 +224,14 @@ fn propagate_positive_domains= new_min_rhs], - conjunction!( - [numerator >= numerator_min] - & [denominator <= denominator_max] - & [denominator >= 1] + ( + conjunction!( + [numerator >= numerator_min] + & [denominator <= denominator_max] + & [denominator >= 1] + ), + inference_code, ), - inference_code, )?; } @@ -241,8 +243,10 @@ fn propagate_positive_domains= new_min_numerator], - conjunction!([denominator >= denominator_min] & [rhs >= rhs_min]), - inference_code, + ( + conjunction!([denominator >= denominator_min] & [rhs >= rhs_min]), + inference_code, + ), )?; } @@ -255,13 +259,15 @@ fn propagate_positive_domains new_max_denominator { context.post( predicate![denominator <= new_max_denominator], - conjunction!( - [numerator <= numerator_max] - & [numerator >= 0] - & [rhs >= rhs_min] - & [denominator >= 1] + ( + conjunction!( + [numerator <= numerator_max] + & [numerator >= 0] + & [rhs >= rhs_min] + & [denominator >= 1] + ), + inference_code, ), - inference_code, )?; } } @@ -279,10 +285,15 @@ fn propagate_positive_domains= new_min_denominator], - conjunction!( - [numerator >= numerator_min] & [rhs <= rhs_max] & [rhs >= 0] & [denominator >= 1] + ( + conjunction!( + [numerator >= numerator_min] + & [rhs <= rhs_max] + & [rhs >= 0] + & [denominator >= 1] + ), + inference_code, ), - inference_code, )?; } @@ -313,8 +324,10 @@ fn propagate_upper_bounds new_max_rhs { context.post( predicate![rhs <= new_max_rhs], - conjunction!([numerator <= numerator_max] & [denominator >= denominator_min]), - inference_code, + ( + conjunction!([numerator <= numerator_max] & [denominator >= denominator_min]), + inference_code, + ), )?; } @@ -327,8 +340,12 @@ fn propagate_upper_bounds new_max_numerator { context.post( predicate![numerator <= new_max_numerator], - conjunction!([denominator <= denominator_max] & [denominator >= 1] & [rhs <= rhs_max]), - inference_code, + ( + conjunction!( + [denominator <= denominator_max] & [denominator >= 1] & [rhs <= rhs_max] + ), + inference_code, + ), )?; } @@ -358,8 +375,10 @@ fn propagate_signs= 0 && rhs_min < 0 { context.post( predicate![rhs >= 0], - conjunction!([numerator >= 0] & [denominator >= 1]), - inference_code, + ( + conjunction!([numerator >= 0] & [denominator >= 1]), + inference_code, + ), )?; } @@ -367,8 +386,10 @@ fn propagate_signs 0 { context.post( predicate![numerator >= 1], - conjunction!([rhs >= 1] & [denominator >= 1]), - inference_code, + ( + conjunction!([rhs >= 1] & [denominator >= 1]), + inference_code, + ), )?; } @@ -376,8 +397,10 @@ fn propagate_signs 0 { context.post( predicate![rhs <= 0], - conjunction!([numerator <= 0] & [denominator >= 1]), - inference_code, + ( + conjunction!([numerator <= 0] & [denominator >= 1]), + inference_code, + ), )?; } @@ -385,8 +408,10 @@ fn propagate_signs= 0 && rhs_max < 0 { context.post( predicate![numerator <= -1], - conjunction!([rhs <= -1] & [denominator >= 1]), - inference_code, + ( + conjunction!([rhs <= -1] & [denominator >= 1]), + inference_code, + ), )?; } @@ -457,28 +482,27 @@ where } } -#[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { - use pumpkin_core::TestSolver; + use pumpkin_core::state::State; use super::*; #[test] fn detects_conflicts() { - let mut solver = TestSolver::default(); - let numerator = solver.new_variable(1, 1); - let denominator = solver.new_variable(2, 2); - let rhs = solver.new_variable(2, 2); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let numerator = state.new_interval_variable(1, 1, None); + let denominator = state.new_interval_variable(2, 2, None); + let rhs = state.new_interval_variable(2, 2, None); + let constraint_tag = state.new_constraint_tag(); - let propagator = solver.new_propagator(DivisionArgs { + let _ = state.add_propagator(DivisionArgs { numerator, denominator, rhs, constraint_tag, }); - assert!(propagator.is_err()); + let _ = state.propagate_to_fixed_point().unwrap_err(); } } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs index 96ad8b234..9e7d8d953 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs @@ -133,15 +133,16 @@ fn perform_propagation= 0] & [a <= a_max] & [b >= 0] & [b <= b_max]), - inference_code, + ( + conjunction!([a >= 0] & [a <= a_max] & [b >= 0] & [b <= b_max]), + inference_code, + ), )?; // c is larger than the minimum value that a * b can take context.post( predicate![c >= new_min_c], - conjunction!([a >= a_min] & [b >= b_min]), - inference_code, + (conjunction!([a >= a_min] & [b >= b_min]), inference_code), )?; } @@ -150,8 +151,10 @@ fn perform_propagation= bound], - conjunction!([c >= c_min] & [b >= 0] & [b <= b_max]), - inference_code, + ( + conjunction!([c >= c_min] & [b >= 0] & [b <= b_max]), + inference_code, + ), )?; } @@ -160,8 +163,10 @@ fn perform_propagation= 0] & [c <= c_max] & [b >= b_min]), - inference_code, + ( + conjunction!([c >= 0] & [c <= c_max] & [b >= b_min]), + inference_code, + ), )?; } @@ -170,8 +175,10 @@ fn perform_propagation= 0] & [c <= c_max] & [a >= a_min]), - inference_code, + ( + conjunction!([c >= 0] & [c <= c_max] & [a >= a_min]), + inference_code, + ), )?; } @@ -181,8 +188,10 @@ fn perform_propagation= bound], - conjunction!([c >= c_min] & [a >= 0] & [a <= a_max]), - inference_code, + ( + conjunction!([c >= c_min] & [a >= 0] & [a <= a_max]), + inference_code, + ), )?; } @@ -247,8 +256,7 @@ fn propagate_signs= 0 && b_min >= 0 { context.post( predicate![c >= 0], - conjunction!([a >= 0] & [b >= 0]), - inference_code, + (conjunction!([a >= 0] & [b >= 0]), inference_code), )?; } @@ -256,8 +264,7 @@ fn propagate_signs= 1 && c_min >= 1 { context.post( predicate![b >= 1], - conjunction!([a >= 1] & [c >= 1]), - inference_code, + (conjunction!([a >= 1] & [c >= 1]), inference_code), )?; } @@ -265,8 +272,7 @@ fn propagate_signs= 1 && c_min >= 1 { context.post( predicate![a >= 1], - conjunction!([b >= 1] & [c >= 1]), - inference_code, + (conjunction!([b >= 1] & [c >= 1]), inference_code), )?; } @@ -275,8 +281,7 @@ fn propagate_signs= 0], - conjunction!([a <= 0] & [b <= 0]), - inference_code, + (conjunction!([a <= 0] & [b <= 0]), inference_code), )?; } @@ -284,8 +289,7 @@ fn propagate_signs= 1], - conjunction!([a <= -1] & [c <= -1]), - inference_code, + (conjunction!([a <= -1] & [c <= -1]), inference_code), )?; } @@ -293,8 +297,7 @@ fn propagate_signs= 1], - conjunction!([b <= -1] & [c <= -1]), - inference_code, + (conjunction!([b <= -1] & [c <= -1]), inference_code), )?; } @@ -304,8 +307,7 @@ fn propagate_signs= 0 { context.post( predicate![c <= 0], - conjunction!([a <= 0] & [b >= 0]), - inference_code, + (conjunction!([a <= 0] & [b >= 0]), inference_code), )?; } @@ -313,8 +315,7 @@ fn propagate_signs= 0 && b_max <= 0 { context.post( predicate![c <= 0], - conjunction!([a >= 0] & [b <= 0]), - inference_code, + (conjunction!([a >= 0] & [b <= 0]), inference_code), )?; } @@ -323,8 +324,7 @@ fn propagate_signs= 1 { context.post( predicate![b <= -1], - conjunction!([a <= -1] & [c >= 1]), - inference_code, + (conjunction!([a <= -1] & [c >= 1]), inference_code), )?; } @@ -332,8 +332,7 @@ fn propagate_signs= 1 && c_max <= -1 { context.post( predicate![b <= -1], - conjunction!([a >= 1] & [c <= -1]), - inference_code, + (conjunction!([a >= 1] & [c <= -1]), inference_code), )?; } @@ -342,8 +341,7 @@ fn propagate_signs= 1 { context.post( predicate![a <= -1], - conjunction!([b <= -1] & [c >= 1]), - inference_code, + (conjunction!([b <= -1] & [c >= 1]), inference_code), )?; } @@ -351,8 +349,7 @@ fn propagate_signs= 1 && c_max <= -1 { context.post( predicate![a <= -1], - conjunction!([b >= 1] & [c <= -1]), - inference_code, + (conjunction!([b >= 1] & [c <= -1]), inference_code), )?; } @@ -416,44 +413,54 @@ where } } -#[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { - use pumpkin_core::TestSolver; + use pumpkin_core::predicate; + use pumpkin_core::predicates::Predicate; + use pumpkin_core::predicates::PropositionalConjunction; + use pumpkin_core::propagation::CurrentNogood; + use pumpkin_core::state::State; use super::*; + use crate::StateExt; #[test] fn bounds_of_a_and_b_propagate_bounds_c() { - let mut solver = TestSolver::default(); - let a = solver.new_variable(1, 3); - let b = solver.new_variable(0, 4); - let c = solver.new_variable(-10, 20); - - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator(IntegerMultiplicationArgs { - a, - b, - c, - constraint_tag, - }) - .expect("no empty domains"); - - solver.propagate(propagator).expect("no empty domains"); - - assert_eq!(1, solver.lower_bound(a)); - assert_eq!(3, solver.upper_bound(a)); - assert_eq!(0, solver.lower_bound(b)); - assert_eq!(4, solver.upper_bound(b)); - assert_eq!(0, solver.lower_bound(c)); - assert_eq!(12, solver.upper_bound(c)); - - let reason_lb = solver.get_reason_int(predicate![c >= 0]); + let mut state = State::default(); + let a = state.new_interval_variable(1, 3, None); + let b = state.new_interval_variable(0, 4, None); + let c = state.new_interval_variable(-10, 20, None); + + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator(IntegerMultiplicationArgs { + a, + b, + c, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domains"); + + state.assert_bounds(a, 1, 3); + state.assert_bounds(b, 0, 4); + state.assert_bounds(c, 0, 12); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![c >= 0], + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason_lb: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([a >= 0] & [b >= 0]), reason_lb); - let reason_ub = solver.get_reason_int(predicate![c <= 12]); + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![c <= 12], + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason_ub: PropositionalConjunction = reason_buffer.into(); assert_eq!( conjunction!([a >= 0] & [a <= 3] & [b >= 0] & [b <= 4]), reason_ub @@ -462,141 +469,149 @@ mod tests { #[test] fn bounds_of_a_and_c_propagate_bounds_b() { - let mut solver = TestSolver::default(); - let a = solver.new_variable(2, 3); - let b = solver.new_variable(0, 12); - let c = solver.new_variable(2, 12); - - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator(IntegerMultiplicationArgs { - a, - b, - c, - constraint_tag, - }) - .expect("no empty domains"); - - solver.propagate(propagator).expect("no empty domains"); - - assert_eq!(2, solver.lower_bound(a)); - assert_eq!(3, solver.upper_bound(a)); - assert_eq!(1, solver.lower_bound(b)); - assert_eq!(6, solver.upper_bound(b)); - assert_eq!(2, solver.lower_bound(c)); - assert_eq!(12, solver.upper_bound(c)); - - let reason_lb = solver.get_reason_int(predicate![b >= 1]); + let mut state = State::default(); + let a = state.new_interval_variable(2, 3, None); + let b = state.new_interval_variable(0, 12, None); + let c = state.new_interval_variable(2, 12, None); + + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator(IntegerMultiplicationArgs { + a, + b, + c, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domains"); + + state.assert_bounds(a, 2, 3); + state.assert_bounds(b, 1, 6); + state.assert_bounds(c, 2, 12); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![b >= 1], + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason_lb: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([a >= 1] & [c >= 1]), reason_lb); - let reason_ub = solver.get_reason_int(predicate![b <= 6]); + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![b <= 6], + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason_ub: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([a >= 2] & [c >= 0] & [c <= 12]), reason_ub); } #[test] fn bounds_of_b_and_c_propagate_bounds_a() { - let mut solver = TestSolver::default(); - let a = solver.new_variable(0, 10); - let b = solver.new_variable(3, 6); - let c = solver.new_variable(2, 12); - - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator(IntegerMultiplicationArgs { - a, - b, - c, - constraint_tag, - }) - .expect("no empty domains"); - - solver.propagate(propagator).expect("no empty domains"); - - assert_eq!(1, solver.lower_bound(a)); - assert_eq!(4, solver.upper_bound(a)); - assert_eq!(3, solver.lower_bound(b)); - assert_eq!(6, solver.upper_bound(b)); - assert_eq!(3, solver.lower_bound(c)); - assert_eq!(12, solver.upper_bound(c)); - - let reason_lb = solver.get_reason_int(predicate![a >= 1]); + let mut state = State::default(); + let a = state.new_interval_variable(0, 10, None); + let b = state.new_interval_variable(3, 6, None); + let c = state.new_interval_variable(2, 12, None); + + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator(IntegerMultiplicationArgs { + a, + b, + c, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domains"); + + state.assert_bounds(a, 1, 4); + state.assert_bounds(b, 3, 6); + state.assert_bounds(c, 3, 12); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![a >= 1], + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason_lb: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([b >= 1] & [c >= 1]), reason_lb); - let reason_ub = solver.get_reason_int(predicate![a <= 4]); + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![a <= 4], + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason_ub: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([b >= 3] & [c >= 0] & [c <= 12]), reason_ub); } #[test] fn b_unbounded_does_not_panic() { - let mut solver = TestSolver::default(); - let a = solver.new_variable(12, 12); - let b = solver.new_variable(i32::MIN, i32::MAX); - let c = solver.new_variable(144, 144); - - let constraint_tag = solver.new_constraint_tag(); - let _ = solver - .new_propagator(IntegerMultiplicationArgs { - a, - b, - c, - constraint_tag, - }) - .expect("No empty domains"); + let mut state = State::default(); + let a = state.new_interval_variable(12, 12, None); + let b = state.new_interval_variable(i32::MIN, i32::MAX, None); + let c = state.new_interval_variable(144, 144, None); + + let constraint_tag = state.new_constraint_tag(); + let _ = state.add_propagator(IntegerMultiplicationArgs { + a, + b, + c, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("No empty domains"); } #[test] fn a_unbounded_does_not_panic() { - let mut solver = TestSolver::default(); - let a = solver.new_variable(i32::MIN, i32::MAX); - let b = solver.new_variable(12, 12); - let c = solver.new_variable(144, 144); - - let constraint_tag = solver.new_constraint_tag(); - let _ = solver - .new_propagator(IntegerMultiplicationArgs { - a, - b, - c, - constraint_tag, - }) - .expect("No empty domains"); + let mut state = State::default(); + let a = state.new_interval_variable(i32::MIN, i32::MAX, None); + let b = state.new_interval_variable(12, 12, None); + let c = state.new_interval_variable(144, 144, None); + + let constraint_tag = state.new_constraint_tag(); + let _ = state.add_propagator(IntegerMultiplicationArgs { + a, + b, + c, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("No empty domains"); } #[test] fn c_unbounded_does_not_panic() { - let mut solver = TestSolver::default(); - let a = solver.new_variable(12, 12); - let b = solver.new_variable(12, 12); - let c = solver.new_variable(i32::MIN, i32::MAX); - - let constraint_tag = solver.new_constraint_tag(); - let _ = solver - .new_propagator(IntegerMultiplicationArgs { - a, - b, - c, - constraint_tag, - }) - .expect("No empty domains"); + let mut state = State::default(); + let a = state.new_interval_variable(12, 12, None); + let b = state.new_interval_variable(12, 12, None); + let c = state.new_interval_variable(i32::MIN, i32::MAX, None); + + let constraint_tag = state.new_constraint_tag(); + let _ = state.add_propagator(IntegerMultiplicationArgs { + a, + b, + c, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("No empty domains"); } #[test] fn all_unbounded_does_not_panic() { - let mut solver = TestSolver::default(); - let a = solver.new_variable(i32::MIN, i32::MAX); - let b = solver.new_variable(i32::MIN, i32::MAX); - let c = solver.new_variable(i32::MIN, i32::MAX); - - let constraint_tag = solver.new_constraint_tag(); - let _ = solver - .new_propagator(IntegerMultiplicationArgs { - a, - b, - c, - constraint_tag, - }) - .expect("No empty domains"); + let mut state = State::default(); + let a = state.new_interval_variable(i32::MIN, i32::MAX, None); + let b = state.new_interval_variable(i32::MIN, i32::MAX, None); + let c = state.new_interval_variable(i32::MIN, i32::MAX, None); + + let constraint_tag = state.new_constraint_tag(); + let _ = state.add_propagator(IntegerMultiplicationArgs { + a, + b, + c, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("No empty domains"); } } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs index 9bbcb2048..075af43d9 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs @@ -15,6 +15,7 @@ use pumpkin_core::propagation::Domains; use pumpkin_core::propagation::EnqueueDecision; use pumpkin_core::propagation::ExplanationContext; use pumpkin_core::propagation::InferenceCheckers; +use pumpkin_core::propagation::LazyExplanation; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::NotificationContext; use pumpkin_core::propagation::OpaqueDomainEvent; @@ -166,7 +167,7 @@ where "LinearLeq" } - fn lazy_explanation(&mut self, code: u64, context: ExplanationContext) -> &[Predicate] { + fn lazy_explanation(&mut self, code: u64, context: ExplanationContext) -> LazyExplanation<'_> { let i = code as usize; self.reason_buffer.clear(); @@ -183,7 +184,10 @@ where } })); - &self.reason_buffer + LazyExplanation { + predicates: self.reason_buffer.as_slice(), + inference_code: self.inference_code.clone(), + } } fn propagate(&mut self, mut context: PropagationContext) -> PropagationStatusCP { @@ -222,7 +226,7 @@ where let bound = self.c - (lower_bound_left_hand_side - context.lower_bound(x_i)); if context.upper_bound(x_i) > bound { - context.post(predicate![x_i <= bound], i, &self.inference_code)?; + context.post(predicate![x_i <= bound], i)?; } } @@ -279,7 +283,7 @@ where }) .collect(); - context.post(predicate![x_i <= bound], reason, &self.inference_code)?; + context.post(predicate![x_i <= bound], (reason, &self.inference_code))?; } } @@ -324,89 +328,95 @@ where } } -#[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { - use pumpkin_core::TestSolver; use pumpkin_core::conjunction; + use pumpkin_core::predicate; + use pumpkin_core::predicates::Predicate; + use pumpkin_core::predicates::PropositionalConjunction; + use pumpkin_core::propagation::CurrentNogood; + use pumpkin_core::state::State; use super::*; + use crate::StateExt; #[test] fn test_bounds_are_propagated() { - let mut solver = TestSolver::default(); - let x = solver.new_variable(1, 5); - let y = solver.new_variable(0, 10); + let mut state = State::default(); + let x = state.new_interval_variable(1, 5, None); + let y = state.new_interval_variable(0, 10, None); - let constraint_tag = solver.new_constraint_tag(); + let constraint_tag = state.new_constraint_tag(); - let propagator = solver - .new_propagator(LinearLessOrEqualPropagatorArgs { - x: [x, y].into(), - c: 7, - constraint_tag, - }) - .expect("no empty domains"); - - solver.propagate(propagator).expect("non-empty domain"); + let _ = state.add_propagator(LinearLessOrEqualPropagatorArgs { + x: [x, y].into(), + c: 7, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domains"); - solver.assert_bounds(x, 1, 5); - solver.assert_bounds(y, 0, 6); + state.assert_bounds(x, 1, 5); + state.assert_bounds(y, 0, 6); } #[test] fn test_explanations() { - let mut solver = TestSolver::default(); - let x = solver.new_variable(1, 5); - let y = solver.new_variable(0, 10); - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator(LinearLessOrEqualPropagatorArgs { - x: [x, y].into(), - c: 7, - constraint_tag, - }) - .expect("no empty domains"); - - solver.propagate(propagator).expect("non-empty domain"); - - let reason = solver.get_reason_int(predicate![y <= 6]); + let mut state = State::default(); + let x = state.new_interval_variable(1, 5, None); + let y = state.new_interval_variable(0, 10, None); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator(LinearLessOrEqualPropagatorArgs { + x: [x, y].into(), + c: 7, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domains"); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![y <= 6], + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([x >= 1]), reason); } #[test] fn overflow_leads_to_conflict() { - let mut solver = TestSolver::default(); - - let x = solver.new_variable(i32::MAX, i32::MAX); - let y = solver.new_variable(1, 1); - let constraint_tag = solver.new_constraint_tag(); - - let _ = solver - .new_propagator(LinearLessOrEqualPropagatorArgs { - x: [x, y].into(), - c: i32::MAX, - constraint_tag, - }) + let mut state = State::default(); + + let x = state.new_interval_variable(i32::MAX, i32::MAX, None); + let y = state.new_interval_variable(1, 1, None); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator(LinearLessOrEqualPropagatorArgs { + x: [x, y].into(), + c: i32::MAX, + constraint_tag, + }); + let _ = state + .propagate_to_fixed_point() .expect_err("Expected overflow to be detected"); } #[test] fn underflow_leads_to_no_propagation() { - let mut solver = TestSolver::default(); - - let x = solver.new_variable(i32::MIN, i32::MIN); - let y = solver.new_variable(-1, -1); - let constraint_tag = solver.new_constraint_tag(); - - let _ = solver - .new_propagator(LinearLessOrEqualPropagatorArgs { - x: [x, y].into(), - c: i32::MIN, - constraint_tag, - }) + let mut state = State::default(); + + let x = state.new_interval_variable(i32::MIN, i32::MIN, None); + let y = state.new_interval_variable(-1, -1, None); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator(LinearLessOrEqualPropagatorArgs { + x: [x, y].into(), + c: i32::MIN, + constraint_tag, + }); + state + .propagate_to_fixed_point() .expect("Expected no error to be detected"); } } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs index a570fc750..130d2b2d8 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs @@ -218,13 +218,15 @@ where context.post( predicate![self.terms[unfixed_x_i] != value_to_remove], - self.terms - .iter() - .enumerate() - .filter(|&(i, _)| i != unfixed_x_i) - .map(|(_, x_i)| predicate![x_i == context.lower_bound(x_i)]) - .collect::(), - &self.inference_code, + ( + self.terms + .iter() + .enumerate() + .filter(|&(i, _)| i != unfixed_x_i) + .map(|(_, x_i)| predicate![x_i == context.lower_bound(x_i)]) + .collect::(), + &self.inference_code, + ), )?; } } else if self.number_of_fixed_terms == self.terms.len() { @@ -275,8 +277,7 @@ where .try_into() .expect("Expected to be able to fit i64 into i32") ], - reason, - &self.inference_code, + (reason, &self.inference_code), )?; } else if num_fixed == self.terms.len() && lhs == self.rhs as i64 { let conjunction = self @@ -387,54 +388,54 @@ where } } -#[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { - use pumpkin_core::TestSolver; use pumpkin_core::conjunction; + use pumpkin_core::predicate; + use pumpkin_core::predicates::Predicate; + use pumpkin_core::predicates::PropositionalConjunction; + use pumpkin_core::propagation::CurrentNogood; use pumpkin_core::state::Conflict; + use pumpkin_core::state::State; use pumpkin_core::variables::TransformableVariable; use super::*; + use crate::StateExt; #[test] fn test_value_is_removed() { - let mut solver = TestSolver::default(); - let x = solver.new_variable(2, 2); - let y = solver.new_variable(1, 5); + let mut state = State::default(); + let x = state.new_interval_variable(2, 2, None); + let y = state.new_interval_variable(1, 5, None); - let constraint_tag = solver.new_constraint_tag(); + let constraint_tag = state.new_constraint_tag(); - let propagator = solver - .new_propagator(LinearNotEqualPropagatorArgs { - terms: [x.scaled(1), y.scaled(-1)].into(), - rhs: 0, - constraint_tag, - }) - .expect("non-empty domain"); - - solver.propagate(propagator).expect("non-empty domain"); + let _ = state.add_propagator(LinearNotEqualPropagatorArgs { + terms: [x.scaled(1), y.scaled(-1)].into(), + rhs: 0, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("non-empty domain"); - solver.assert_bounds(x, 2, 2); - solver.assert_bounds(y, 1, 5); - assert!(!solver.contains(y, 2)); + state.assert_bounds(x, 2, 2); + state.assert_bounds(y, 1, 5); + assert!(!state.contains(y, 2)); } #[test] fn test_empty_domain_is_detected() { - let mut solver = TestSolver::default(); - let x = solver.new_variable(2, 2); - let y = solver.new_variable(2, 2); + let mut state = State::default(); + let x = state.new_interval_variable(2, 2, None); + let y = state.new_interval_variable(2, 2, None); - let constraint_tag = solver.new_constraint_tag(); + let constraint_tag = state.new_constraint_tag(); - let err = solver - .new_propagator(LinearNotEqualPropagatorArgs { - terms: [x.scaled(1), y.scaled(-1)].into(), - rhs: 0, - constraint_tag, - }) - .expect_err("empty domain"); + let _ = state.add_propagator(LinearNotEqualPropagatorArgs { + terms: [x.scaled(1), y.scaled(-1)].into(), + rhs: 0, + constraint_tag, + }); + let err = state.propagate_to_fixed_point().expect_err("empty domain"); let expected = conjunction!([x == 2] & [y == 2]); @@ -446,53 +447,52 @@ mod tests { #[test] fn explanation_for_propagation() { - let mut solver = TestSolver::default(); - let x = solver.new_variable(2, 2).scaled(1); - let y = solver.new_variable(1, 5).scaled(-1); - - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator(LinearNotEqualPropagatorArgs { - terms: [x, y].into(), - rhs: 0, - constraint_tag, - }) - .expect("non-empty domain"); + let mut state = State::default(); + let x = state.new_interval_variable(2, 2, None).scaled(1); + let y = state.new_interval_variable(1, 5, None).scaled(-1); - solver.propagate(propagator).expect("non-empty domain"); + let constraint_tag = state.new_constraint_tag(); - let reason = solver.get_reason_int(predicate![y != -2]); + let _ = state.add_propagator(LinearNotEqualPropagatorArgs { + terms: [x, y].into(), + rhs: 0, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("non-empty domain"); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![y != -2], + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([x == 2]), reason); } #[test] fn satisfied_constraint_does_not_trigger_conflict() { - let mut solver = TestSolver::default(); - let x = solver.new_variable(0, 3); - let y = solver.new_variable(0, 3); + let mut state = State::default(); + let x = state.new_interval_variable(0, 3, None); + let y = state.new_interval_variable(0, 3, None); - let constraint_tag = solver.new_constraint_tag(); + let constraint_tag = state.new_constraint_tag(); - let propagator = solver - .new_propagator(LinearNotEqualPropagatorArgs { - terms: [x.scaled(1), y.scaled(-1)].into(), - rhs: 0, - constraint_tag, - }) - .expect("non-empty domain"); - - solver.remove(x, 0).expect("non-empty domain"); - solver.remove(x, 2).expect("non-empty domain"); - solver.remove(x, 3).expect("non-empty domain"); + let _ = state.add_propagator(LinearNotEqualPropagatorArgs { + terms: [x.scaled(1), y.scaled(-1)].into(), + rhs: 0, + constraint_tag, + }); - solver.remove(y, 0).expect("non-empty domain"); - solver.remove(y, 1).expect("non-empty domain"); - solver.remove(y, 2).expect("non-empty domain"); + let _ = state.post(predicate![x != 0]).unwrap(); + let _ = state.post(predicate![x != 2]).unwrap(); + let _ = state.post(predicate![x != 3]).unwrap(); - solver.notify_propagator(propagator); + let _ = state.post(predicate![y != 0]).unwrap(); + let _ = state.post(predicate![y != 1]).unwrap(); + let _ = state.post(predicate![y != 2]).unwrap(); - solver.propagate(propagator).expect("non-empty domain"); + state.propagate_to_fixed_point().expect("non-empty domain"); } } diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs index ff2d98814..2a847bc4c 100644 --- a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs +++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs @@ -106,8 +106,7 @@ impl Prop // UB(a_i) <= UB(rhs, constraint_tag } context.post( predicate![var <= rhs_ub], - conjunction!([self.rhs <= rhs_ub]), - &self.inference_code, + (conjunction!([self.rhs <= rhs_ub]), &self.inference_code), )?; let var_lb = context.lower_bound(var); @@ -126,8 +125,10 @@ impl Prop // LB(rhs, constraint_tag } >= max{LB(a_i)}. context.post( predicate![self.rhs >= max_lb], - PropositionalConjunction::from(lb_reason), - &self.inference_code, + ( + PropositionalConjunction::from(lb_reason), + &self.inference_code, + ), )?; // Rule 3. @@ -142,8 +143,7 @@ impl Prop .collect(); context.post( predicate![self.rhs <= max_ub], - ub_reason, - &self.inference_code, + (ub_reason, &self.inference_code), )?; } @@ -175,8 +175,7 @@ impl Prop propagation_reason.push(predicate![self.rhs >= rhs_lb]); context.post( predicate![propagating_variable >= rhs_lb], - propagation_reason, - &self.inference_code, + (propagation_reason, &self.inference_code), )?; } } @@ -224,109 +223,128 @@ where } } -#[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { - use pumpkin_core::TestSolver; + use pumpkin_core::predicate; + use pumpkin_core::predicates::Predicate; + use pumpkin_core::predicates::PropositionalConjunction; + use pumpkin_core::propagation::CurrentNogood; + use pumpkin_core::state::State; use super::*; + use crate::StateExt; #[test] fn upper_bound_of_rhs_matches_maximum_upper_bound_of_array_at_initialise() { - let mut solver = TestSolver::default(); + let mut state = State::default(); - let a = solver.new_variable(1, 3); - let b = solver.new_variable(1, 4); - let c = solver.new_variable(1, 5); + let a = state.new_interval_variable(1, 3, None); + let b = state.new_interval_variable(1, 4, None); + let c = state.new_interval_variable(1, 5, None); - let rhs = solver.new_variable(1, 10); - let constraint_tag = solver.new_constraint_tag(); + let rhs = state.new_interval_variable(1, 10, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(MaximumArgs { - array: [a, b, c].into(), - rhs, - constraint_tag, - }) - .expect("no empty domain"); + let _ = state.add_propagator(MaximumArgs { + array: [a, b, c].into(), + rhs, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domain"); - solver.assert_bounds(rhs, 1, 5); + state.assert_bounds(rhs, 1, 5); - let reason = solver.get_reason_int(predicate![rhs <= 5]); + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![rhs <= 5], + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([a <= 5] & [b <= 5] & [c <= 5]), reason); } #[test] fn lower_bound_of_rhs_is_maximum_of_lower_bounds_in_array() { - let mut solver = TestSolver::default(); + let mut state = State::default(); - let a = solver.new_variable(3, 10); - let b = solver.new_variable(4, 10); - let c = solver.new_variable(5, 10); + let a = state.new_interval_variable(3, 10, None); + let b = state.new_interval_variable(4, 10, None); + let c = state.new_interval_variable(5, 10, None); - let rhs = solver.new_variable(1, 10); - let constraint_tag = solver.new_constraint_tag(); + let rhs = state.new_interval_variable(1, 10, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(MaximumArgs { - array: [a, b, c].into(), - rhs, - constraint_tag, - }) - .expect("no empty domain"); + let _ = state.add_propagator(MaximumArgs { + array: [a, b, c].into(), + rhs, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domain"); - solver.assert_bounds(rhs, 5, 10); + state.assert_bounds(rhs, 5, 10); - let reason = solver.get_reason_int(predicate![rhs >= 5]); + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![rhs >= 5], + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([c >= 5]), reason); } #[test] fn upper_bound_of_all_array_elements_at_most_rhs_max_at_initialise() { - let mut solver = TestSolver::default(); + let mut state = State::default(); let array = (1..=5) - .map(|idx| solver.new_variable(1, 4 + idx)) + .map(|idx| state.new_interval_variable(1, 4 + idx, None)) .collect::>(); - let rhs = solver.new_variable(1, 3); - let constraint_tag = solver.new_constraint_tag(); + let rhs = state.new_interval_variable(1, 3, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(MaximumArgs { - array: array.clone(), - rhs, - constraint_tag, - }) - .expect("no empty domain"); + let _ = state.add_propagator(MaximumArgs { + array: array.clone(), + rhs, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domain"); for var in array.iter() { - solver.assert_bounds(*var, 1, 3); - let reason = solver.get_reason_int(predicate![var <= 3]); + state.assert_bounds(*var, 1, 3); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![var <= 3], + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([rhs <= 3]), reason); } } #[test] fn single_variable_propagate() { - let mut solver = TestSolver::default(); + let mut state = State::default(); let array = (1..=5) - .map(|idx| solver.new_variable(1, 1 + 10 * idx)) + .map(|idx| state.new_interval_variable(1, 1 + 10 * idx, None)) .collect::>(); - let rhs = solver.new_variable(45, 60); - let constraint_tag = solver.new_constraint_tag(); + let rhs = state.new_interval_variable(45, 60, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(MaximumArgs { - array: array.clone(), - rhs, - constraint_tag, - }) - .expect("no empty domain"); + let _ = state.add_propagator(MaximumArgs { + array: array.clone(), + rhs, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domain"); - solver.assert_bounds(*array.last().unwrap(), 45, 51); - solver.assert_bounds(rhs, 45, 51); + state.assert_bounds(*array.last().unwrap(), 45, 51); + state.assert_bounds(rhs, 45, 51); } } diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs index 83790b842..d475463d6 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/checker.rs @@ -6,8 +6,6 @@ use pumpkin_checking::InferenceChecker; use pumpkin_checking::IntExt; use pumpkin_checking::VariableState; -use crate::cumulative::time_table::time_table_util::has_overlap_with_interval; - #[derive(Clone, Debug)] pub struct TimeTableChecker { pub tasks: Box<[CheckerTask]>, @@ -21,66 +19,12 @@ pub struct CheckerTask { pub processing_time: i32, } -fn lower_bound_can_be_propagated_by_profile< - Var: CheckerVariable, - Atomic: AtomicConstraint, ->( - context: &VariableState, - lower_bound: i32, +fn can_be_propagated_by_profile, Atomic: AtomicConstraint>( task: &CheckerTask, - start: i32, - end: i32, height: i32, capacity: i32, ) -> bool { - let upper_bound = task - .start_time - .induced_upper_bound(context) - .try_into() - .unwrap(); - height + task.resource_usage > capacity - && !(upper_bound < (lower_bound + task.processing_time) - && has_overlap_with_interval( - upper_bound, - lower_bound + task.processing_time, - start, - end, - )) - && has_overlap_with_interval(lower_bound, upper_bound + task.processing_time, start, end) - && (lower_bound + task.processing_time) > start - && lower_bound <= end -} - -fn upper_bound_can_be_propagated_by_profile< - Var: CheckerVariable, - Atomic: AtomicConstraint, ->( - context: &VariableState, - upper_bound: i32, - task: &CheckerTask, - start: i32, - end: i32, - height: i32, - capacity: i32, -) -> bool { - let lower_bound = task - .start_time - .induced_lower_bound(context) - .try_into() - .unwrap(); - - height + task.resource_usage > capacity - && !(upper_bound < (lower_bound + task.processing_time) - && has_overlap_with_interval( - upper_bound, - lower_bound + task.processing_time, - start, - end, - )) - && has_overlap_with_interval(lower_bound, upper_bound + task.processing_time, start, end) - && (upper_bound + task.processing_time) > end - && upper_bound <= end } impl InferenceChecker for TimeTableChecker @@ -90,7 +34,7 @@ where { fn check( &self, - state: VariableState, + mut state: VariableState, _: &[Atomic], consequent: Option<&Atomic>, ) -> bool { @@ -117,30 +61,12 @@ where .try_into() .unwrap(); - if lst < est + task.processing_time { - *profile.entry(lst).or_insert(0) += task.resource_usage; - *profile.entry(est + task.processing_time).or_insert(0) -= task.resource_usage; - } - } - - let mut profiles = Vec::new(); - let mut current_usage = 0; - let mut previous_time_point = *profile - .first_key_value() - .expect("Expected at least one mandatory part") - .0; - for (time_point, usage) in profile.iter() { - if current_usage > 0 && *time_point != previous_time_point { - profiles.push((previous_time_point, *time_point - 1, current_usage)) - } - - current_usage += *usage; - - if current_usage > self.capacity { - return true; + for t in lst..est + task.processing_time { + *profile.entry(t).or_insert(0) += task.resource_usage; + if *profile.get(&t).unwrap() > self.capacity { + return true; + } } - - previous_time_point = *time_point; } if let Some(propagating_task) = consequent.map(|consequent| { @@ -149,48 +75,32 @@ where .find(|task| task.start_time.does_atomic_constrain_self(consequent)) .expect("If there is a consequent, then there should be a propagating task") }) { - let mut lower_bound: i32 = propagating_task + let lst: i32 = propagating_task .start_time - .induced_lower_bound(&state) + .induced_upper_bound(&state) .try_into() .unwrap(); - for (start, end_inclusive, height) in profiles.iter() { - if lower_bound_can_be_propagated_by_profile( - &state, - lower_bound, - propagating_task, - *start, - *end_inclusive, - *height, - self.capacity, - ) { - lower_bound = end_inclusive + 1; - } - } - if lower_bound > propagating_task.start_time.induced_upper_bound(&state) { - return true; - } - - let mut upper_bound: i32 = propagating_task + let est: i32 = propagating_task .start_time - .induced_upper_bound(&state) + .induced_lower_bound(&state) .try_into() .unwrap(); - for (start, end_inclusive, height) in profiles.iter().rev() { - if upper_bound_can_be_propagated_by_profile( - &state, - upper_bound, - propagating_task, - *start, - *end_inclusive, - *height, - self.capacity, - ) { - upper_bound = start - propagating_task.processing_time; + + for t in lst..est + propagating_task.processing_time { + *profile.entry(t).or_insert(0) -= propagating_task.resource_usage; + if *profile.get(&t).unwrap() > self.capacity { + return true; } } - if upper_bound < propagating_task.start_time.induced_lower_bound(&state) { - return true; + + for (t, height) in profile.iter() { + if can_be_propagated_by_profile(propagating_task, *height, self.capacity) { + for t in (t - propagating_task.processing_time + 1)..=*t { + if !state.apply(&propagating_task.start_time.atomic_not_equal(t)) { + return true; + } + } + } } } false @@ -573,4 +483,67 @@ mod tests { assert!(checker.check(state, &premises, consequent.as_ref())); } + + #[test] + fn test_holes_in_domain() { + let premises = [ + TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 1, + }, + TestAtomic { + name: "x3", + comparison: pumpkin_checking::Comparison::LessEqual, + value: 3, + }, + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 4, + }, + TestAtomic { + name: "x1", + comparison: pumpkin_checking::Comparison::LessEqual, + value: 4, + }, + TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 2, + }, + ]; + + let consequent = Some(TestAtomic { + name: "x2", + comparison: pumpkin_checking::Comparison::GreaterEqual, + value: 5, + }); + let state = VariableState::prepare_for_conflict_check(premises, consequent) + .expect("no conflicting atomics"); + + let checker = TimeTableChecker { + tasks: vec![ + CheckerTask { + start_time: "x3", + resource_usage: 1, + processing_time: 3, + }, + CheckerTask { + start_time: "x2", + resource_usage: 2, + processing_time: 2, + }, + CheckerTask { + start_time: "x1", + resource_usage: 1, + processing_time: 1, + }, + ] + .into(), + capacity: 2, + }; + + assert!(checker.check(state, &premises, consequent.as_ref())); + } } diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/explanations/pointwise.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/explanations/pointwise.rs index 822d620e5..bf22e81f7 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/explanations/pointwise.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/explanations/pointwise.rs @@ -85,8 +85,7 @@ pub(crate) fn propagate_lower_bounds_with_pointwise_explanations= time_point + 1], - reason, - inference_code, + (reason, inference_code), )?; } @@ -203,8 +202,7 @@ pub(crate) fn propagate_upper_bounds_with_pointwise_explanations( }) } -#[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { + #[allow( + deprecated, + reason = "TestSolver is deprecated but still used in these tests" + )] use pumpkin_core::TestSolver; use pumpkin_core::conjunction; use pumpkin_core::predicate; use pumpkin_core::predicates::Predicate; + use pumpkin_core::predicates::PropositionalConjunction; + use pumpkin_core::propagation::CurrentNogood; use pumpkin_core::propagation::EnqueueDecision; use pumpkin_core::state::Conflict; + use pumpkin_core::state::State; use pumpkin_core::variables::DomainId; + use crate::StateExt; use crate::cumulative::ArgTask; use crate::cumulative::options::CumulativePropagatorOptions; use crate::cumulative::time_table::CumulativeExplanationType; @@ -663,188 +670,187 @@ mod tests { #[test] fn propagator_propagates_from_profile() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(1, 1); - let s2 = solver.new_variable(1, 8); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(1, 1, None); + let s2 = state.new_interval_variable(1, 8, None); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator( + TimeTableOverIntervalIncrementalPropagator::::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, + }, + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions::default(), + constraint_tag, + ), + ); - let _ = solver - .new_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 5); - assert_eq!(solver.upper_bound(s2), 8); - assert_eq!(solver.lower_bound(s1), 1); - assert_eq!(solver.upper_bound(s1), 1); + state.propagate_to_fixed_point().expect("No conflict"); + + state.assert_bounds(s1, 1, 1); + state.assert_bounds(s2, 5, 8); } #[test] fn propagator_detects_conflict() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(1, 1); - let s2 = solver.new_variable(1, 1); - let constraint_tag = solver.new_constraint_tag(); - - let result = solver.new_propagator(TimeTableOverIntervalIncrementalPropagator::< - DomainId, - false, - >::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 4, - resource_usage: 1, + let mut state = State::default(); + let s1 = state.new_interval_variable(1, 1, None); + let s2 = state.new_interval_variable(1, 1, None); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator( + TimeTableOverIntervalIncrementalPropagator::::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, + }, + ArgTask { + start_time: s2, + processing_time: 4, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + ..Default::default() }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )); - - assert!(matches!(result, Err(Conflict::Propagator(_)))); - assert!(match result { - Err(Conflict::Propagator(conflict)) => { - let expected = [ - predicate!(s1 <= 1), - predicate!(s1 >= 1), - predicate!(s2 >= 1), - predicate!(s2 <= 1), - ]; - expected.iter().all(|y| { - conflict - .conjunction - .iter() - .collect::>() - .contains(&y) - }) && conflict.conjunction.iter().all(|y| expected.contains(y)) - } - _ => false, - }); + constraint_tag, + ), + ); + + let Conflict::Propagator(conflict) = state.propagate_to_fixed_point().unwrap_err() else { + panic!("an explicit conflict should be detected"); + }; + + let expected = [ + predicate!(s1 <= 1), + predicate!(s1 >= 1), + predicate!(s2 >= 1), + predicate!(s2 <= 1), + ]; + + assert!(expected.iter().all(|y| { + conflict + .conjunction + .iter() + .collect::>() + .contains(&y) + })); + + assert!(conflict.conjunction.iter().all(|y| expected.contains(y))); } #[test] fn propagator_propagates_nothing() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(0, 6); - let s2 = solver.new_variable(0, 6); - let constraint_tag = solver.new_constraint_tag(); - - let _ = solver - .new_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 0); - assert_eq!(solver.upper_bound(s2), 6); - assert_eq!(solver.lower_bound(s1), 0); - assert_eq!(solver.upper_bound(s1), 6); + let mut state = State::default(); + let s1 = state.new_interval_variable(0, 6, None); + let s2 = state.new_interval_variable(0, 6, None); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator( + TimeTableOverIntervalIncrementalPropagator::::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, + }, + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions::default(), + constraint_tag, + ), + ); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 0, 6); + state.assert_bounds(s2, 0, 6); } #[test] fn propagator_propagates_example_4_3_schutt() { - let mut solver = TestSolver::default(); - let f = solver.new_variable(0, 14); - let e = solver.new_variable(2, 4); - let d = solver.new_variable(0, 2); - let c = solver.new_variable(8, 9); - let b = solver.new_variable(2, 3); - let a = solver.new_variable(0, 1); - let constraint_tag = solver.new_constraint_tag(); - - let _ = solver - .new_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b, - processing_time: 6, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 5, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(f), 10); + let mut state = State::default(); + let f = state.new_interval_variable(0, 14, None); + let e = state.new_interval_variable(2, 4, None); + let d = state.new_interval_variable(0, 2, None); + let c = state.new_interval_variable(8, 9, None); + let b = state.new_interval_variable(2, 3, None); + let a = state.new_interval_variable(0, 1, None); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator( + TimeTableOverIntervalIncrementalPropagator::::new( + &[ + ArgTask { + start_time: a, + processing_time: 2, + resource_usage: 1, + }, + ArgTask { + start_time: b, + processing_time: 6, + resource_usage: 2, + }, + ArgTask { + start_time: c, + processing_time: 2, + resource_usage: 4, + }, + ArgTask { + start_time: d, + processing_time: 2, + resource_usage: 2, + }, + ArgTask { + start_time: e, + processing_time: 5, + resource_usage: 2, + }, + ArgTask { + start_time: f, + processing_time: 6, + resource_usage: 2, + }, + ] + .into_iter() + .collect::>(), + 5, + CumulativePropagatorOptions::default(), + constraint_tag, + ), + ); + state.propagate_to_fixed_point().expect("No conflict"); + assert_eq!(state.lower_bound(f), 10); } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn propagator_propagates_after_assignment() { let mut solver = TestSolver::default(); let s1 = solver.new_variable(0, 6); @@ -894,47 +900,54 @@ mod tests { #[test] fn propagator_propagates_end_time() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(6, 6); - let s2 = solver.new_variable(1, 8); - let constraint_tag = solver.new_constraint_tag(); - - let _ = solver - .new_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() + let mut state = State::default(); + let s1 = state.new_interval_variable(6, 6, None); + let s2 = state.new_interval_variable(1, 8, None); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator( + TimeTableOverIntervalIncrementalPropagator::::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, }, - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 1); - assert_eq!(solver.upper_bound(s2), 3); - assert_eq!(solver.lower_bound(s1), 6); - assert_eq!(solver.upper_bound(s1), 6); - - let reason = solver.get_reason_int(predicate!(s2 <= 3)); + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + ..Default::default() + }, + constraint_tag, + ), + ); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 6, 6); + state.assert_bounds(s2, 1, 3); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s2 <= 3), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([s2 <= 8] & [s1 >= 6] & [s1 <= 6]), reason); } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn propagator_propagates_example_4_3_schutt_after_update() { let mut solver = TestSolver::default(); let f = solver.new_variable(0, 14); @@ -1012,6 +1025,10 @@ mod tests { } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn propagator_propagates_example_4_3_schutt_multiple_profiles() { let mut solver = TestSolver::default(); let f = solver.new_variable(0, 14); @@ -1094,138 +1111,147 @@ mod tests { #[test] fn propagator_propagates_from_profile_reason() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(1, 1); - let s2 = solver.new_variable(1, 8); - let constraint_tag = solver.new_constraint_tag(); - - let _ = solver - .new_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() + let mut state = State::default(); + let s1 = state.new_interval_variable(1, 1, None); + let s2 = state.new_interval_variable(1, 8, None); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator( + TimeTableOverIntervalIncrementalPropagator::::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, }, - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 5); - assert_eq!(solver.upper_bound(s2), 8); - assert_eq!(solver.lower_bound(s1), 1); - assert_eq!(solver.upper_bound(s1), 1); - - let reason = solver.get_reason_int(predicate!(s2 >= 5)); + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + ..Default::default() + }, + constraint_tag, + ), + ); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 1, 1); + state.assert_bounds(s2, 5, 8); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s2 >= 5), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([s2 >= 1] & [s1 >= 1] & [s1 <= 1]), reason); } #[test] fn propagator_propagates_generic_bounds() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(3, 3); - let s2 = solver.new_variable(5, 5); - let s3 = solver.new_variable(1, 15); - let constraint_tag = solver.new_constraint_tag(); - - let _ = solver - .new_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s3, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() + let mut state = State::default(); + let s1 = state.new_interval_variable(3, 3, None); + let s2 = state.new_interval_variable(5, 5, None); + let s3 = state.new_interval_variable(1, 15, None); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator( + TimeTableOverIntervalIncrementalPropagator::::new( + &[ + ArgTask { + start_time: s1, + processing_time: 2, + resource_usage: 1, }, - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s3), 7); - assert_eq!(solver.upper_bound(s3), 15); - assert_eq!(solver.lower_bound(s2), 5); - assert_eq!(solver.upper_bound(s2), 5); - assert_eq!(solver.lower_bound(s1), 3); - assert_eq!(solver.upper_bound(s1), 3); - - let reason = solver.get_reason_int(predicate!(s3 >= 7)); + ArgTask { + start_time: s2, + processing_time: 2, + resource_usage: 1, + }, + ArgTask { + start_time: s3, + processing_time: 4, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + ..Default::default() + }, + constraint_tag, + ), + ); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 3, 3); + state.assert_bounds(s2, 5, 5); + state.assert_bounds(s3, 7, 15); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s3 >= 7), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([s2 <= 5] & [s2 >= 5] & [s3 >= 5]), reason); } #[test] fn propagator_propagates_with_holes() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(4, 4); - let s2 = solver.new_variable(0, 8); - let constraint_tag = solver.new_constraint_tag(); - - let _ = solver - .new_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - allow_holes_in_domain: true, - ..Default::default() + let mut state = State::default(); + let s1 = state.new_interval_variable(4, 4, None); + let s2 = state.new_interval_variable(0, 8, None); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator( + TimeTableOverIntervalIncrementalPropagator::::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, }, - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 0); - assert_eq!(solver.upper_bound(s2), 8); - assert_eq!(solver.lower_bound(s1), 4); - assert_eq!(solver.upper_bound(s1), 4); + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + allow_holes_in_domain: true, + ..Default::default() + }, + constraint_tag, + ), + ); + state.propagate_to_fixed_point().expect("No conflict"); + + state.assert_bounds(s1, 4, 4); + state.assert_bounds(s2, 0, 8); for removed in 2..8 { - assert!(!solver.contains(s2, removed)); - let reason = solver.get_reason_int(predicate!(s2 != removed)); + assert!(!state.contains(s2, removed)); + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s2 != removed), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([s1 <= 4] & [s1 >= 4]), reason); } } diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/synchronisation.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/synchronisation.rs index c46bf9c33..1892cb749 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/synchronisation.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/synchronisation.rs @@ -160,13 +160,12 @@ fn sort_profile_based_on_id(profile: &mut Resour profile.profile_tasks.sort_by_key(|task| task.id.unpack()); } -#[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { use std::rc::Rc; - use pumpkin_core::TestSolver; use pumpkin_core::propagation::LocalId; + use pumpkin_core::state::State; use super::find_synchronised_conflict; use crate::cumulative::CumulativeParameters; @@ -177,11 +176,11 @@ mod tests { #[test] fn test_correct_conflict_returned() { - let mut solver = TestSolver::default(); + let mut state = State::default(); - let x0 = solver.new_variable(0, 10); - let x1 = solver.new_variable(0, 10); - let x2 = solver.new_variable(0, 10); + let x0 = state.new_interval_variable(0, 10, None); + let x1 = state.new_interval_variable(0, 10, None); + let x2 = state.new_interval_variable(0, 10, None); let tasks = vec![ Task { diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs index eb51b9ea8..9b4ffa1e5 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs @@ -642,17 +642,24 @@ mod debug { } } -#[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { + #[allow( + deprecated, + reason = "TestSolver is deprecated but still used in these tests" + )] use pumpkin_core::TestSolver; use pumpkin_core::conjunction; use pumpkin_core::predicate; use pumpkin_core::predicates::Predicate; + use pumpkin_core::predicates::PropositionalConjunction; + use pumpkin_core::propagation::CurrentNogood; use pumpkin_core::propagation::EnqueueDecision; use pumpkin_core::state::Conflict; + use pumpkin_core::state::State; use pumpkin_core::variables::DomainId; + use crate::StateExt; use crate::cumulative::ArgTask; use crate::cumulative::options::CumulativePropagatorOptions; use crate::cumulative::time_table::CumulativeExplanationType; @@ -661,48 +668,45 @@ mod tests { #[test] fn propagator_propagates_from_profile() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(1, 1); - let s2 = solver.new_variable(1, 8); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(1, 1, None); + let s2 = state.new_interval_variable(1, 8, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 5); - assert_eq!(solver.upper_bound(s2), 8); - assert_eq!(solver.lower_bound(s1), 1); - assert_eq!(solver.upper_bound(s1), 1); + let _ = state.add_propagator( + TimeTablePerPointIncrementalPropagator::::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, + }, + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions::default(), + constraint_tag, + ), + ); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 1, 1); + state.assert_bounds(s2, 5, 8); } #[test] fn propagator_detects_conflict() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(1, 1); - let s2 = solver.new_variable(1, 1); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(1, 1, None); + let s2 = state.new_interval_variable(1, 1, None); + let constraint_tag = state.new_constraint_tag(); - let result = solver.new_propagator( + let _ = state.add_propagator( TimeTablePerPointIncrementalPropagator::::new( &[ ArgTask { @@ -726,119 +730,121 @@ mod tests { constraint_tag, ), ); - assert!(match result { - Err(Conflict::Propagator(x)) => { - let expected = [ - predicate!(s1 <= 1), - predicate!(s1 >= 1), - predicate!(s2 <= 1), - predicate!(s2 >= 1), - ]; - expected.iter().all(|y| { - x.conjunction - .iter() - .collect::>() - .contains(&y) - }) && x.conjunction.iter().all(|y| expected.contains(y)) - } - _ => false, - }); + let Conflict::Propagator(x) = state.propagate_to_fixed_point().unwrap_err() else { + panic!("an explicit conflict should have been detected"); + }; + + let expected = [ + predicate!(s1 <= 1), + predicate!(s1 >= 1), + predicate!(s2 <= 1), + predicate!(s2 >= 1), + ]; + + assert!(expected.iter().all(|y| { + x.conjunction + .iter() + .collect::>() + .contains(&y) + })); + + assert!(x.conjunction.iter().all(|y| expected.contains(y))); } #[test] fn propagator_propagates_nothing() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(0, 6); - let s2 = solver.new_variable(0, 6); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(0, 6, None); + let s2 = state.new_interval_variable(0, 6, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 0); - assert_eq!(solver.upper_bound(s2), 6); - assert_eq!(solver.lower_bound(s1), 0); - assert_eq!(solver.upper_bound(s1), 6); + let _ = state.add_propagator( + TimeTablePerPointIncrementalPropagator::::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, + }, + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions::default(), + constraint_tag, + ), + ); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 0, 6); + state.assert_bounds(s2, 0, 6); } #[test] fn propagator_propagates_example_4_3_schutt() { - let mut solver = TestSolver::default(); - let f = solver.new_variable(0, 14); - let e = solver.new_variable(2, 4); - let d = solver.new_variable(0, 2); - let c = solver.new_variable(8, 9); - let b = solver.new_variable(2, 3); - let a = solver.new_variable(0, 1); - let constraint_tag = solver.new_constraint_tag(); - - let _ = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b, - processing_time: 6, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 5, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(f), 10); + let mut state = State::default(); + let f = state.new_interval_variable(0, 14, None); + let e = state.new_interval_variable(2, 4, None); + let d = state.new_interval_variable(0, 2, None); + let c = state.new_interval_variable(8, 9, None); + let b = state.new_interval_variable(2, 3, None); + let a = state.new_interval_variable(0, 1, None); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator( + TimeTablePerPointIncrementalPropagator::::new( + &[ + ArgTask { + start_time: a, + processing_time: 2, + resource_usage: 1, + }, + ArgTask { + start_time: b, + processing_time: 6, + resource_usage: 2, + }, + ArgTask { + start_time: c, + processing_time: 2, + resource_usage: 4, + }, + ArgTask { + start_time: d, + processing_time: 2, + resource_usage: 2, + }, + ArgTask { + start_time: e, + processing_time: 5, + resource_usage: 2, + }, + ArgTask { + start_time: f, + processing_time: 6, + resource_usage: 2, + }, + ] + .into_iter() + .collect::>(), + 5, + CumulativePropagatorOptions::default(), + constraint_tag, + ), + ); + state.propagate_to_fixed_point().expect("No conflict"); + assert_eq!(state.lower_bound(f), 10); } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn propagator_propagates_after_assignment() { let mut solver = TestSolver::default(); let s1 = solver.new_variable(0, 6); @@ -888,49 +894,54 @@ mod tests { #[test] fn propagator_propagates_end_time() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(6, 6); - let s2 = solver.new_variable(1, 8); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(6, 6, None); + let s2 = state.new_interval_variable(1, 8, None); + let constraint_tag = state.new_constraint_tag(); - let propagator = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() + let _ = state.add_propagator( + TimeTablePerPointIncrementalPropagator::::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, }, - constraint_tag, - ), - ) - .expect("No conflict"); - let result = solver.propagate_until_fixed_point(propagator); - assert!(result.is_ok()); - assert_eq!(solver.lower_bound(s2), 1); - assert_eq!(solver.upper_bound(s2), 3); - assert_eq!(solver.lower_bound(s1), 6); - assert_eq!(solver.upper_bound(s1), 6); - - let reason = solver.get_reason_int(predicate!(s2 <= 3)); + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + ..Default::default() + }, + constraint_tag, + ), + ); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 6, 6); + state.assert_bounds(s2, 1, 3); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s2 <= 3), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([s2 <= 5] & [s1 >= 6] & [s1 <= 6]), reason); } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn propagator_propagates_example_4_3_schutt_after_update() { let mut solver = TestSolver::default(); let f = solver.new_variable(0, 14); @@ -1008,6 +1019,10 @@ mod tests { } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn propagator_propagates_example_4_3_schutt_multiple_profiles() { let mut solver = TestSolver::default(); let f = solver.new_variable(0, 14); @@ -1090,43 +1105,46 @@ mod tests { #[test] fn propagator_propagates_from_profile_reason() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(1, 1); - let s2 = solver.new_variable(1, 8); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(1, 1, None); + let s2 = state.new_interval_variable(1, 8, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() + let _ = state.add_propagator( + TimeTablePerPointIncrementalPropagator::::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, }, - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 5); - assert_eq!(solver.upper_bound(s2), 8); - assert_eq!(solver.lower_bound(s1), 1); - assert_eq!(solver.upper_bound(s1), 1); - - let reason = solver.get_reason_int(predicate!(s2 >= 5)); + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + ..Default::default() + }, + constraint_tag, + ), + ); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 1, 1); + state.assert_bounds(s2, 5, 8); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s2 >= 5), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!( conjunction!([s2 >= 4] & [s1 >= 1] & [s1 <= 1]), /* Note that this not * the most general @@ -1142,51 +1160,53 @@ mod tests { #[test] fn propagator_propagates_generic_bounds() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(3, 3); - let s2 = solver.new_variable(5, 5); - let s3 = solver.new_variable(1, 15); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(3, 3, None); + let s2 = state.new_interval_variable(5, 5, None); + let s3 = state.new_interval_variable(1, 15, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s3, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() + let _ = state.add_propagator( + TimeTablePerPointIncrementalPropagator::::new( + &[ + ArgTask { + start_time: s1, + processing_time: 2, + resource_usage: 1, }, - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s3), 7); - assert_eq!(solver.upper_bound(s3), 15); - assert_eq!(solver.lower_bound(s2), 5); - assert_eq!(solver.upper_bound(s2), 5); - assert_eq!(solver.lower_bound(s1), 3); - assert_eq!(solver.upper_bound(s1), 3); - - let reason = solver.get_reason_int(predicate!(s3 >= 7)); + ArgTask { + start_time: s2, + processing_time: 2, + resource_usage: 1, + }, + ArgTask { + start_time: s3, + processing_time: 4, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + ..Default::default() + }, + constraint_tag, + ), + ); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 3, 3); + state.assert_bounds(s2, 5, 5); + state.assert_bounds(s3, 7, 15); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s3 >= 7), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!( conjunction!([s2 <= 5] & [s2 >= 5] & [s3 >= 6]), /* Note that s3 would * have been able to @@ -1199,51 +1219,58 @@ mod tests { #[test] fn propagator_propagates_with_holes() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(4, 4); - let s2 = solver.new_variable(0, 8); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(4, 4, None); + let s2 = state.new_interval_variable(0, 8, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - allow_holes_in_domain: true, - ..Default::default() + let _ = state.add_propagator( + TimeTablePerPointIncrementalPropagator::::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, }, - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 0); - assert_eq!(solver.upper_bound(s2), 8); - assert_eq!(solver.lower_bound(s1), 4); - assert_eq!(solver.upper_bound(s1), 4); + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + allow_holes_in_domain: true, + ..Default::default() + }, + constraint_tag, + ), + ); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 4, 4); + state.assert_bounds(s2, 0, 8); for removed in 2..8 { - assert!(!solver.contains(s2, removed)); - let reason = solver.get_reason_int(predicate!(s2 != removed)); + assert!(!state.contains(s2, removed)); + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s2 != removed), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([s1 <= 4] & [s1 >= 4]), reason); } } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn synchronisation_leads_to_same_conflict_explanation() { let mut solver_scratch = TestSolver::default(); let s1_scratch = solver_scratch.new_variable(5, 5); @@ -1342,6 +1369,10 @@ mod tests { } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn synchronisation_leads_to_same_conflict_after_propagating() { let mut solver_scratch = TestSolver::default(); let s1_scratch = solver_scratch.new_variable(5, 5); @@ -1439,6 +1470,10 @@ mod tests { } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn no_synchronisation_leads_to_different_conflict_explanation() { let mut solver_scratch = TestSolver::default(); let s1_scratch = solver_scratch.new_variable(5, 5); @@ -1534,6 +1569,10 @@ mod tests { } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn synchronisation_leads_to_same_explanation() { let mut solver_scratch = TestSolver::default(); let s1_scratch = solver_scratch.new_variable(1, 6); @@ -1628,6 +1667,10 @@ mod tests { ); } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn no_synchronisation_leads_to_different_explanation() { let mut solver_scratch = TestSolver::default(); let s1_scratch = solver_scratch.new_variable(1, 6); @@ -1725,6 +1768,10 @@ mod tests { } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn synchronisation_leads_to_same_conflict() { let mut solver_scratch = TestSolver::default(); let s0_scratch = solver_scratch.new_variable(1, 11); diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/propagation_handler.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/propagation_handler.rs index eb2abf689..93e38fae9 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/propagation_handler.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/propagation_handler.rs @@ -147,7 +147,7 @@ impl CumulativePropagationHandler { &full_explanation, context.domains() )); - context.post(predicate, full_explanation, &self.inference_code) + context.post(predicate, (full_explanation, &self.inference_code)) } CumulativeExplanationType::Pointwise => { pointwise::propagate_lower_bounds_with_pointwise_explanations( @@ -226,7 +226,7 @@ impl CumulativePropagationHandler { &full_explanation, context.domains() )); - context.post(predicate, full_explanation, &self.inference_code) + context.post(predicate, (full_explanation, &self.inference_code)) } CumulativeExplanationType::Pointwise => { pointwise::propagate_upper_bounds_with_pointwise_explanations( @@ -281,7 +281,7 @@ impl CumulativePropagationHandler { pumpkin_assert_extreme!(check_explanation(predicate, &reason, context.domains())); - context.post(predicate, reason, &self.inference_code) + context.post(predicate, (reason, &self.inference_code)) } CumulativeExplanationType::Pointwise => { pointwise::propagate_lower_bounds_with_pointwise_explanations( @@ -340,7 +340,7 @@ impl CumulativePropagationHandler { pumpkin_assert_extreme!(check_explanation(predicate, &reason, context.domains())); - context.post(predicate, reason, &self.inference_code) + context.post(predicate, (reason, &self.inference_code)) } CumulativeExplanationType::Pointwise => { pointwise::propagate_upper_bounds_with_pointwise_explanations( @@ -413,7 +413,7 @@ impl CumulativePropagationHandler { &explanation, context.domains() )); - context.post(predicate, (*explanation).clone(), &self.inference_code)?; + context.post(predicate, ((*explanation).clone(), &self.inference_code))?; } CumulativeExplanationType::Pointwise => { // We split into two cases when determining the explanation of the profile @@ -449,7 +449,7 @@ impl CumulativePropagationHandler { &explanation, context.domains() )); - context.post(predicate, explanation, &self.inference_code)?; + context.post(predicate, (explanation, &self.inference_code))?; } } } diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs index e1b7ef124..cb8b6926b 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs @@ -487,16 +487,23 @@ pub(crate) fn propagate_from_scratch_time_table_interval>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 5); - assert_eq!(solver.upper_bound(s2), 8); - assert_eq!(solver.lower_bound(s1), 1); - assert_eq!(solver.upper_bound(s1), 1); + let _ = state.add_propagator(TimeTableOverIntervalPropagator::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, + }, + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions::default(), + constraint_tag, + )); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 1, 1); + state.assert_bounds(s2, 5, 8); } #[test] fn propagator_detects_conflict() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(1, 1); - let s2 = solver.new_variable(1, 1); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(1, 1, None); + let s2 = state.new_interval_variable(1, 1, None); + let constraint_tag = state.new_constraint_tag(); - let result = solver.new_propagator(TimeTableOverIntervalPropagator::new( + let _ = state.add_propagator(TimeTableOverIntervalPropagator::new( &[ ArgTask { start_time: s1, @@ -566,120 +570,117 @@ mod tests { constraint_tag, )); - assert!(match result { - Err(e) => { - match e { - Conflict::EmptyDomain(_) => false, - Conflict::Propagator(x) => { - let expected = [ - predicate!(s1 <= 1), - predicate!(s1 >= 1), - predicate!(s2 >= 1), - predicate!(s2 <= 1), - ]; - expected.iter().all(|y| { - x.conjunction - .iter() - .collect::>() - .contains(&y) - }) && x.conjunction.iter().all(|y| expected.contains(y)) - } - } - } - _ => false, - }); + let Conflict::Propagator(x) = state.propagate_to_fixed_point().unwrap_err() else { + panic!("an explicit conflict should have been detected"); + }; + + let expected = [ + predicate!(s1 <= 1), + predicate!(s1 >= 1), + predicate!(s2 >= 1), + predicate!(s2 <= 1), + ]; + + assert!(expected.iter().all(|y| { + x.conjunction + .iter() + .collect::>() + .contains(&y) + })); + + assert!(x.conjunction.iter().all(|y| expected.contains(y))); } #[test] fn propagator_propagates_nothing() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(0, 6); - let s2 = solver.new_variable(0, 6); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(0, 6, None); + let s2 = state.new_interval_variable(0, 6, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 0); - assert_eq!(solver.upper_bound(s2), 6); - assert_eq!(solver.lower_bound(s1), 0); - assert_eq!(solver.upper_bound(s1), 6); + let _ = state.add_propagator(TimeTableOverIntervalPropagator::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, + }, + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions::default(), + constraint_tag, + )); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 0, 6); + state.assert_bounds(s2, 0, 6); } #[test] fn propagator_propagates_example_4_3_schutt() { - let mut solver = TestSolver::default(); - let f = solver.new_variable(0, 14); - let e = solver.new_variable(2, 4); - let d = solver.new_variable(0, 2); - let c = solver.new_variable(8, 9); - let b = solver.new_variable(2, 3); - let a = solver.new_variable(0, 1); - let constraint_tag = solver.new_constraint_tag(); - - let _ = solver - .new_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b, - processing_time: 6, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 5, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(f), 10); + let mut state = State::default(); + let f = state.new_interval_variable(0, 14, None); + let e = state.new_interval_variable(2, 4, None); + let d = state.new_interval_variable(0, 2, None); + let c = state.new_interval_variable(8, 9, None); + let b = state.new_interval_variable(2, 3, None); + let a = state.new_interval_variable(0, 1, None); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator(TimeTableOverIntervalPropagator::new( + &[ + ArgTask { + start_time: a, + processing_time: 2, + resource_usage: 1, + }, + ArgTask { + start_time: b, + processing_time: 6, + resource_usage: 2, + }, + ArgTask { + start_time: c, + processing_time: 2, + resource_usage: 4, + }, + ArgTask { + start_time: d, + processing_time: 2, + resource_usage: 2, + }, + ArgTask { + start_time: e, + processing_time: 5, + resource_usage: 2, + }, + ArgTask { + start_time: f, + processing_time: 6, + resource_usage: 2, + }, + ] + .into_iter() + .collect::>(), + 5, + CumulativePropagatorOptions::default(), + constraint_tag, + )); + state.propagate_to_fixed_point().expect("No conflict"); + assert_eq!(state.lower_bound(f), 10); } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn propagator_propagates_after_assignment() { let mut solver = TestSolver::default(); let s1 = solver.new_variable(0, 6); @@ -707,10 +708,8 @@ mod tests { constraint_tag, )) .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 6); - assert_eq!(solver.upper_bound(s2), 10); - assert_eq!(solver.lower_bound(s1), 0); - assert_eq!(solver.upper_bound(s1), 6); + solver.assert_bounds(s1, 0, 6); + solver.assert_bounds(s2, 6, 10); let notification_status = solver.increase_lower_bound_and_notify(propagator, 0, s1, 5); assert!(match notification_status { EnqueueDecision::Enqueue => true, @@ -719,61 +718,68 @@ mod tests { let result = solver.propagate(propagator); assert!(result.is_ok()); - assert_eq!(solver.lower_bound(s2), 7); - assert_eq!(solver.upper_bound(s2), 10); - assert_eq!(solver.lower_bound(s1), 5); - assert_eq!(solver.upper_bound(s1), 6); + solver.assert_bounds(s1, 5, 6); + solver.assert_bounds(s2, 7, 10); } #[test] fn propagator_propagates_end_time() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(6, 6); - let s2 = solver.new_variable(1, 8); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(6, 6, None); + let s2 = state.new_interval_variable(1, 8, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() + let _ = state.add_propagator(TimeTableOverIntervalPropagator::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, }, - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 1); - assert_eq!(solver.upper_bound(s2), 3); - assert_eq!(solver.lower_bound(s1), 6); - assert_eq!(solver.upper_bound(s1), 6); - - let reason = solver.get_reason_int(predicate!(s2 <= 3)); + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + ..Default::default() + }, + constraint_tag, + )); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 6, 6); + state.assert_bounds(s2, 1, 3); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s2 <= 3), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([s2 <= 8] & [s1 >= 6] & [s1 <= 6]), reason); } #[test] - fn propagator_propagates_example_4_3_schutt_after_update() { + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] + fn propagator_propagates_example_4_3_schutt_multiple_profiles() { let mut solver = TestSolver::default(); let f = solver.new_variable(0, 14); let e = solver.new_variable(0, 4); let d = solver.new_variable(0, 2); let c = solver.new_variable(8, 9); - let b = solver.new_variable(2, 3); + let b2 = solver.new_variable(5, 5); + let b1 = solver.new_variable(3, 3); let a = solver.new_variable(0, 1); + let constraint_tag = solver.new_constraint_tag(); let propagator = solver @@ -785,8 +791,13 @@ mod tests { resource_usage: 1, }, ArgTask { - start_time: b, - processing_time: 6, + start_time: b1, + processing_time: 2, + resource_usage: 2, + }, + ArgTask { + start_time: b2, + processing_time: 3, resource_usage: 2, }, ArgTask { @@ -817,20 +828,13 @@ mod tests { constraint_tag, )) .expect("No conflict"); - assert_eq!(solver.lower_bound(a), 0); - assert_eq!(solver.upper_bound(a), 1); - assert_eq!(solver.lower_bound(b), 2); - assert_eq!(solver.upper_bound(b), 3); - assert_eq!(solver.lower_bound(c), 8); - assert_eq!(solver.upper_bound(c), 9); - assert_eq!(solver.lower_bound(d), 0); - assert_eq!(solver.upper_bound(d), 2); - assert_eq!(solver.lower_bound(e), 0); - assert_eq!(solver.upper_bound(e), 4); - assert_eq!(solver.lower_bound(f), 0); - assert_eq!(solver.upper_bound(f), 14); + solver.assert_bounds(a, 0, 1); + solver.assert_bounds(c, 8, 9); + solver.assert_bounds(d, 0, 2); + solver.assert_bounds(e, 0, 4); + solver.assert_bounds(f, 0, 14); - let notification_status = solver.increase_lower_bound_and_notify(propagator, 3, e, 3); + let notification_status = solver.increase_lower_bound_and_notify(propagator, 4, e, 3); assert!(match notification_status { EnqueueDecision::Enqueue => true, EnqueueDecision::Skip => false, @@ -841,16 +845,61 @@ mod tests { } #[test] - fn propagator_propagates_example_4_3_schutt_multiple_profiles() { + fn propagator_propagates_from_profile_reason() { + let mut state = State::default(); + let s1 = state.new_interval_variable(1, 1, None); + let s2 = state.new_interval_variable(1, 8, None); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator(TimeTableOverIntervalPropagator::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, + }, + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + ..Default::default() + }, + constraint_tag, + )); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 1, 1); + state.assert_bounds(s2, 5, 8); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s2 >= 5), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); + assert_eq!(conjunction!([s2 >= 1] & [s1 >= 1] & [s1 <= 1]), reason); + } + + #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] + fn propagator_propagates_example_4_3_schutt_after_update() { let mut solver = TestSolver::default(); let f = solver.new_variable(0, 14); let e = solver.new_variable(0, 4); let d = solver.new_variable(0, 2); let c = solver.new_variable(8, 9); - let b2 = solver.new_variable(5, 5); - let b1 = solver.new_variable(3, 3); + let b = solver.new_variable(2, 3); let a = solver.new_variable(0, 1); - let constraint_tag = solver.new_constraint_tag(); let propagator = solver @@ -862,13 +911,8 @@ mod tests { resource_usage: 1, }, ArgTask { - start_time: b1, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: b2, - processing_time: 3, + start_time: b, + processing_time: 6, resource_usage: 2, }, ArgTask { @@ -899,18 +943,14 @@ mod tests { constraint_tag, )) .expect("No conflict"); - assert_eq!(solver.lower_bound(a), 0); - assert_eq!(solver.upper_bound(a), 1); - assert_eq!(solver.lower_bound(c), 8); - assert_eq!(solver.upper_bound(c), 9); - assert_eq!(solver.lower_bound(d), 0); - assert_eq!(solver.upper_bound(d), 2); - assert_eq!(solver.lower_bound(e), 0); - assert_eq!(solver.upper_bound(e), 4); - assert_eq!(solver.lower_bound(f), 0); - assert_eq!(solver.upper_bound(f), 14); + solver.assert_bounds(a, 0, 1); + solver.assert_bounds(b, 2, 3); + solver.assert_bounds(c, 8, 9); + solver.assert_bounds(d, 0, 2); + solver.assert_bounds(e, 0, 4); + solver.assert_bounds(f, 0, 14); - let notification_status = solver.increase_lower_bound_and_notify(propagator, 4, e, 3); + let notification_status = solver.increase_lower_bound_and_notify(propagator, 3, e, 3); assert!(match notification_status { EnqueueDecision::Enqueue => true, EnqueueDecision::Skip => false, @@ -920,134 +960,99 @@ mod tests { assert_eq!(solver.lower_bound(f), 10); } - #[test] - fn propagator_propagates_from_profile_reason() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(1, 1); - let s2 = solver.new_variable(1, 8); - let constraint_tag = solver.new_constraint_tag(); - - let _ = solver - .new_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 5); - assert_eq!(solver.upper_bound(s2), 8); - assert_eq!(solver.lower_bound(s1), 1); - assert_eq!(solver.upper_bound(s1), 1); - - let reason = solver.get_reason_int(predicate!(s2 >= 5)); - assert_eq!(conjunction!([s2 >= 1] & [s1 >= 1] & [s1 <= 1]), reason); - } - #[test] fn propagator_propagates_generic_bounds() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(3, 3); - let s2 = solver.new_variable(5, 5); - let s3 = solver.new_variable(1, 15); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(3, 3, None); + let s2 = state.new_interval_variable(5, 5, None); + let s3 = state.new_interval_variable(1, 15, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s3, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() + let _ = state.add_propagator(TimeTableOverIntervalPropagator::new( + &[ + ArgTask { + start_time: s1, + processing_time: 2, + resource_usage: 1, }, - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s3), 7); - assert_eq!(solver.upper_bound(s3), 15); - assert_eq!(solver.lower_bound(s2), 5); - assert_eq!(solver.upper_bound(s2), 5); - assert_eq!(solver.lower_bound(s1), 3); - assert_eq!(solver.upper_bound(s1), 3); - - let reason = solver.get_reason_int(predicate!(s3 >= 7)); + ArgTask { + start_time: s2, + processing_time: 2, + resource_usage: 1, + }, + ArgTask { + start_time: s3, + processing_time: 4, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + ..Default::default() + }, + constraint_tag, + )); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 3, 3); + state.assert_bounds(s2, 5, 5); + state.assert_bounds(s3, 7, 15); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s3 >= 7), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([s2 <= 5] & [s2 >= 5] & [s3 >= 5]), reason); } #[test] fn propagator_propagates_with_holes() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(4, 4); - let s2 = solver.new_variable(0, 8); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(4, 4, None); + let s2 = state.new_interval_variable(0, 8, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - allow_holes_in_domain: true, - ..Default::default() + let _ = state.add_propagator(TimeTableOverIntervalPropagator::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, }, - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 0); - assert_eq!(solver.upper_bound(s2), 8); - assert_eq!(solver.lower_bound(s1), 4); - assert_eq!(solver.upper_bound(s1), 4); + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + allow_holes_in_domain: true, + ..Default::default() + }, + constraint_tag, + )); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 4, 4); + state.assert_bounds(s2, 0, 8); for removed in 2..8 { - assert!(!solver.contains(s2, removed)); - let reason = solver.get_reason_int(predicate!(s2 != removed)); + assert!(!state.contains(s2, removed)); + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s2 != removed), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([s1 <= 4] & [s1 >= 4]), reason); } } diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs index af251068f..5451cd01d 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs @@ -293,16 +293,23 @@ pub(crate) fn propagate_from_scratch_time_table_point>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 5); - assert_eq!(solver.upper_bound(s2), 8); - assert_eq!(solver.lower_bound(s1), 1); - assert_eq!(solver.upper_bound(s1), 1); + let _ = state.add_propagator(TimeTablePerPointPropagator::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, + }, + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions::default(), + constraint_tag, + )); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 1, 1); + state.assert_bounds(s2, 5, 8); } #[test] fn propagator_detects_conflict() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(1, 1); - let s2 = solver.new_variable(1, 1); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(1, 1, None); + let s2 = state.new_interval_variable(1, 1, None); + let constraint_tag = state.new_constraint_tag(); - let result = solver.new_propagator(TimeTablePerPointPropagator::new( + let _ = state.add_propagator(TimeTablePerPointPropagator::new( &[ ArgTask { start_time: s1, @@ -371,119 +375,114 @@ mod tests { }, constraint_tag, )); - assert!(match result { - Err(e) => match e { - Conflict::EmptyDomain(_) => false, - Conflict::Propagator(x) => { - let expected = [ - predicate!(s1 <= 1), - predicate!(s1 >= 1), - predicate!(s2 <= 1), - predicate!(s2 >= 1), - ]; - expected.iter().all(|y| { - x.conjunction - .iter() - .collect::>() - .contains(&y) - }) && x.conjunction.iter().all(|y| expected.contains(y)) - } - }, - - Ok(_) => false, - }); + let Conflict::Propagator(x) = state.propagate_to_fixed_point().unwrap_err() else { + panic!("an explicit conflict should have been detected"); + }; + let expected = [ + predicate!(s1 <= 1), + predicate!(s1 >= 1), + predicate!(s2 <= 1), + predicate!(s2 >= 1), + ]; + assert!(expected.iter().all(|y| { + x.conjunction + .iter() + .collect::>() + .contains(&y) + })); + assert!(x.conjunction.iter().all(|y| expected.contains(y))); } #[test] fn propagator_propagates_nothing() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(0, 6); - let s2 = solver.new_variable(0, 6); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(0, 6, None); + let s2 = state.new_interval_variable(0, 6, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 0); - assert_eq!(solver.upper_bound(s2), 6); - assert_eq!(solver.lower_bound(s1), 0); - assert_eq!(solver.upper_bound(s1), 6); + let _ = state.add_propagator(TimeTablePerPointPropagator::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, + }, + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions::default(), + constraint_tag, + )); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 0, 6); + state.assert_bounds(s2, 0, 6); } #[test] fn propagator_propagates_example_4_3_schutt() { - let mut solver = TestSolver::default(); - let f = solver.new_variable(0, 14); - let e = solver.new_variable(2, 4); - let d = solver.new_variable(0, 2); - let c = solver.new_variable(8, 9); - let b = solver.new_variable(2, 3); - let a = solver.new_variable(0, 1); - let constraint_tag = solver.new_constraint_tag(); - - let _ = solver - .new_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b, - processing_time: 6, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 5, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(f), 10); + let mut state = State::default(); + let f = state.new_interval_variable(0, 14, None); + let e = state.new_interval_variable(2, 4, None); + let d = state.new_interval_variable(0, 2, None); + let c = state.new_interval_variable(8, 9, None); + let b = state.new_interval_variable(2, 3, None); + let a = state.new_interval_variable(0, 1, None); + let constraint_tag = state.new_constraint_tag(); + + let _ = state.add_propagator(TimeTablePerPointPropagator::new( + &[ + ArgTask { + start_time: a, + processing_time: 2, + resource_usage: 1, + }, + ArgTask { + start_time: b, + processing_time: 6, + resource_usage: 2, + }, + ArgTask { + start_time: c, + processing_time: 2, + resource_usage: 4, + }, + ArgTask { + start_time: d, + processing_time: 2, + resource_usage: 2, + }, + ArgTask { + start_time: e, + processing_time: 5, + resource_usage: 2, + }, + ArgTask { + start_time: f, + processing_time: 6, + resource_usage: 2, + }, + ] + .into_iter() + .collect::>(), + 5, + CumulativePropagatorOptions::default(), + constraint_tag, + )); + state.propagate_to_fixed_point().expect("No conflict"); + assert_eq!(state.lower_bound(f), 10); } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn propagator_propagates_after_assignment() { let mut solver = TestSolver::default(); let s1 = solver.new_variable(0, 6); @@ -531,47 +530,52 @@ mod tests { #[test] fn propagator_propagates_end_time() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(6, 6); - let s2 = solver.new_variable(1, 8); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(6, 6, None); + let s2 = state.new_interval_variable(1, 8, None); + let constraint_tag = state.new_constraint_tag(); - let propagator = solver - .new_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() + let _ = state.add_propagator(TimeTablePerPointPropagator::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, }, - constraint_tag, - )) - .expect("No conflict"); - let result = solver.propagate_until_fixed_point(propagator); - assert!(result.is_ok()); - assert_eq!(solver.lower_bound(s2), 1); - assert_eq!(solver.upper_bound(s2), 3); - assert_eq!(solver.lower_bound(s1), 6); - assert_eq!(solver.upper_bound(s1), 6); - - let reason = solver.get_reason_int(predicate!(s2 <= 3)); + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + ..Default::default() + }, + constraint_tag, + )); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 6, 6); + state.assert_bounds(s2, 1, 3); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s2 <= 3), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([s2 <= 5] & [s1 >= 6] & [s1 <= 6]), reason); } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn propagator_propagates_example_4_3_schutt_after_update() { let mut solver = TestSolver::default(); let f = solver.new_variable(0, 14); @@ -647,6 +651,10 @@ mod tests { } #[test] + #[allow( + deprecated, + reason = "Uses TestSolver for incremental notification assertions" + )] fn propagator_propagates_example_4_3_schutt_multiple_profiles() { let mut solver = TestSolver::default(); let f = solver.new_variable(0, 14); @@ -727,41 +735,44 @@ mod tests { #[test] fn propagator_propagates_from_profile_reason() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(1, 1); - let s2 = solver.new_variable(1, 8); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(1, 1, None); + let s2 = state.new_interval_variable(1, 8, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() + let _ = state.add_propagator(TimeTablePerPointPropagator::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, }, - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 5); - assert_eq!(solver.upper_bound(s2), 8); - assert_eq!(solver.lower_bound(s1), 1); - assert_eq!(solver.upper_bound(s1), 1); - - let reason = solver.get_reason_int(predicate!(s2 >= 5)); + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + ..Default::default() + }, + constraint_tag, + )); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 1, 1); + state.assert_bounds(s2, 5, 8); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s2 >= 5), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!( conjunction!([s2 >= 4] & [s1 >= 1] & [s1 <= 1]), /* Note that this not * the most general @@ -777,49 +788,51 @@ mod tests { #[test] fn propagator_propagates_generic_bounds() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(3, 3); - let s2 = solver.new_variable(5, 5); - let s3 = solver.new_variable(1, 15); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(3, 3, None); + let s2 = state.new_interval_variable(5, 5, None); + let s3 = state.new_interval_variable(1, 15, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s3, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() + let _ = state.add_propagator(TimeTablePerPointPropagator::new( + &[ + ArgTask { + start_time: s1, + processing_time: 2, + resource_usage: 1, }, - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s3), 7); - assert_eq!(solver.upper_bound(s3), 15); - assert_eq!(solver.lower_bound(s2), 5); - assert_eq!(solver.upper_bound(s2), 5); - assert_eq!(solver.lower_bound(s1), 3); - assert_eq!(solver.upper_bound(s1), 3); - - let reason = solver.get_reason_int(predicate!(s3 >= 7)); + ArgTask { + start_time: s2, + processing_time: 2, + resource_usage: 1, + }, + ArgTask { + start_time: s3, + processing_time: 4, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + ..Default::default() + }, + constraint_tag, + )); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 3, 3); + state.assert_bounds(s2, 5, 5); + state.assert_bounds(s3, 7, 15); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s3 >= 7), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!( conjunction!([s2 <= 5] & [s2 >= 5] & [s3 >= 6]), /* Note that s3 would * have been able to @@ -832,44 +845,47 @@ mod tests { #[test] fn propagator_propagates_with_holes() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(4, 4); - let s2 = solver.new_variable(0, 8); - let constraint_tag = solver.new_constraint_tag(); + let mut state = State::default(); + let s1 = state.new_interval_variable(4, 4, None); + let s2 = state.new_interval_variable(0, 8, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - allow_holes_in_domain: true, - ..Default::default() + let _ = state.add_propagator(TimeTablePerPointPropagator::new( + &[ + ArgTask { + start_time: s1, + processing_time: 4, + resource_usage: 1, }, - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 0); - assert_eq!(solver.upper_bound(s2), 8); - assert_eq!(solver.lower_bound(s1), 4); - assert_eq!(solver.upper_bound(s1), 4); + ArgTask { + start_time: s2, + processing_time: 3, + resource_usage: 1, + }, + ] + .into_iter() + .collect::>(), + 1, + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + allow_holes_in_domain: true, + ..Default::default() + }, + constraint_tag, + )); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 4, 4); + state.assert_bounds(s2, 0, 8); for removed in 2..8 { - assert!(!solver.contains(s2, removed)); - let reason = solver.get_reason_int(predicate!(s2 != removed)); + assert!(!state.contains(s2, removed)); + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate!(s2 != removed), + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!(conjunction!([s1 <= 4] & [s1 >= 4]), reason); } } diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/utils/structs/updatable_structures.rs b/pumpkin-crates/propagators/src/propagators/cumulative/utils/structs/updatable_structures.rs index 00a667395..314271ba4 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/utils/structs/updatable_structures.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/utils/structs/updatable_structures.rs @@ -30,10 +30,14 @@ pub(crate) struct UpdatableStructures { impl UpdatableStructures { pub(crate) fn new(parameters: &CumulativeParameters) -> Self { - let mut updated_tasks = SparseSet::new(parameters.tasks.to_vec(), Task::get_id); + let mut updated_tasks = SparseSet::new_with_mapping(parameters.tasks.to_vec(), |element| { + Task::get_id(element) as i32 + }); updated_tasks.set_to_empty(); - let unfixed_tasks = SparseSet::new(parameters.tasks.to_vec(), Task::get_id); + let unfixed_tasks = SparseSet::new_with_mapping(parameters.tasks.to_vec(), |element| { + Task::get_id(element) as i32 + }); Self { bounds: vec![], updates: vec![], diff --git a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs index 0fc7df192..0d2da6002 100644 --- a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs +++ b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs @@ -221,15 +221,17 @@ fn edge_finding( let propagated_predicate = predicate!(propagated_variable >= new_bound); context.post( propagated_predicate, - create_propagation_explanation( - tasks, - i, - theta_lambda_tree, - context, - new_bound, - lct_j, + ( + create_propagation_explanation( + tasks, + i, + theta_lambda_tree, + context, + new_bound, + lct_j, + ), + inference_code, ), - inference_code, )?; } @@ -416,46 +418,44 @@ fn create_propagation_explanation<'a, Var: IntegerVariable>( explanation.into() } -#[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { - use pumpkin_core::TestSolver; + use pumpkin_core::state::State; use crate::disjunctive::ArgDisjunctiveTask; use crate::disjunctive::DisjunctiveConstructor; #[test] fn propagator_propagates_lower_bound() { - let mut solver = TestSolver::default(); - let c = solver.new_variable(4, 26); - let d = solver.new_variable(13, 13); - let e = solver.new_variable(5, 10); - let f = solver.new_variable(5, 10); - - let constraint_tag = solver.new_constraint_tag(); - let _ = solver - .new_propagator(DisjunctiveConstructor::new( - [ - ArgDisjunctiveTask { - start_time: c, - processing_time: 4, - }, - ArgDisjunctiveTask { - start_time: d, - processing_time: 5, - }, - ArgDisjunctiveTask { - start_time: e, - processing_time: 3, - }, - ArgDisjunctiveTask { - start_time: f, - processing_time: 3, - }, - ], - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(c), 18); + let mut state = State::default(); + let c = state.new_interval_variable(4, 26, None); + let d = state.new_interval_variable(13, 13, None); + let e = state.new_interval_variable(5, 10, None); + let f = state.new_interval_variable(5, 10, None); + + let constraint_tag = state.new_constraint_tag(); + let _ = state.add_propagator(DisjunctiveConstructor::new( + [ + ArgDisjunctiveTask { + start_time: c, + processing_time: 4, + }, + ArgDisjunctiveTask { + start_time: d, + processing_time: 5, + }, + ArgDisjunctiveTask { + start_time: e, + processing_time: 3, + }, + ArgDisjunctiveTask { + start_time: f, + processing_time: 3, + }, + ], + constraint_tag, + )); + state.propagate_to_fixed_point().expect("No conflict"); + assert_eq!(state.lower_bound(c), 18); } } diff --git a/pumpkin-crates/propagators/src/propagators/disjunctive/theta_lambda_tree.rs b/pumpkin-crates/propagators/src/propagators/disjunctive/theta_lambda_tree.rs index e8c8e2834..2519a0f8c 100644 --- a/pumpkin-crates/propagators/src/propagators/disjunctive/theta_lambda_tree.rs +++ b/pumpkin-crates/propagators/src/propagators/disjunctive/theta_lambda_tree.rs @@ -374,11 +374,10 @@ impl ThetaLambdaTree { } } -#[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { - use pumpkin_core::TestSolver; use pumpkin_core::propagation::LocalId; + use pumpkin_core::state::State; use crate::disjunctive::theta_lambda_tree::DisjunctiveTask; use crate::propagators::disjunctive::theta_lambda_tree::Node; @@ -386,11 +385,11 @@ mod tests { #[test] fn tree_built_correctly() { - let mut solver = TestSolver::default(); - let a = solver.new_variable(0, 0); - let b = solver.new_variable(25, 25); - let c = solver.new_variable(30, 30); - let d = solver.new_variable(32, 32); + let mut state = State::default(); + let a = state.new_interval_variable(0, 0, None); + let b = state.new_interval_variable(25, 25, None); + let c = state.new_interval_variable(30, 30, None); + let d = state.new_interval_variable(32, 32, None); let tasks = [ DisjunctiveTask { start_time: a, @@ -416,12 +415,12 @@ mod tests { let mut tree = ThetaLambdaTree::new(&tasks); - tree.update(solver.state.get_domains()); + tree.update(state.get_domains()); for task in tasks.iter() { - tree.add_to_theta(task, solver.state.get_domains()); + tree.add_to_theta(task, state.get_domains()); } tree.remove_from_theta(&tasks[2]); - tree.add_to_lambda(&tasks[2], solver.state.get_domains()); + tree.add_to_lambda(&tasks[2], state.get_domains()); assert_eq!( tree.nodes[6], diff --git a/pumpkin-crates/propagators/src/propagators/element.rs b/pumpkin-crates/propagators/src/propagators/element.rs index 1015af362..8874a259c 100644 --- a/pumpkin-crates/propagators/src/propagators/element.rs +++ b/pumpkin-crates/propagators/src/propagators/element.rs @@ -19,6 +19,7 @@ use pumpkin_core::proof::InferenceCode; use pumpkin_core::propagation::DomainEvents; use pumpkin_core::propagation::ExplanationContext; use pumpkin_core::propagation::InferenceCheckers; +use pumpkin_core::propagation::LazyExplanation; use pumpkin_core::propagation::LocalId; use pumpkin_core::propagation::Priority; use pumpkin_core::propagation::PropagationContext; @@ -138,7 +139,7 @@ where Ok(()) } - fn lazy_explanation(&mut self, code: u64, context: ExplanationContext) -> &[Predicate] { + fn lazy_explanation(&mut self, code: u64, context: ExplanationContext) -> LazyExplanation<'_> { let payload = RightHandSideReason::from_bits(code); self.rhs_reason_buffer.clear(); @@ -158,7 +159,10 @@ where } })); - &self.rhs_reason_buffer + LazyExplanation { + predicates: self.rhs_reason_buffer.as_slice(), + inference_code: self.inference_code.clone(), + } } } @@ -175,13 +179,11 @@ where ) -> PropagationStatusCP { context.post( predicate![self.index >= 0], - conjunction!(), - &self.inference_code, + (conjunction!(), &self.inference_code), )?; context.post( predicate![self.index <= self.array.len() as i32 - 1], - conjunction!(), - &self.inference_code, + (conjunction!(), &self.inference_code), )?; Ok(()) } @@ -212,7 +214,6 @@ where .with_value(rhs_lb) .into_bits(), ), - &self.inference_code, )?; context.post( predicate![self.rhs <= rhs_ub], @@ -222,7 +223,6 @@ where .with_value(rhs_ub) .into_bits(), ), - &self.inference_code, )?; Ok(()) @@ -255,7 +255,10 @@ where } for (idx, reason) in to_remove.drain(..) { - context.post(predicate![self.index != idx], reason, &self.inference_code)?; + context.post( + predicate![self.index != idx], + (reason, &self.inference_code), + )?; } Ok(()) @@ -274,13 +277,17 @@ where context.post( predicate![lhs >= rhs_lb], - conjunction!([self.rhs >= rhs_lb] & [self.index == index]), - &self.inference_code, + ( + conjunction!([self.rhs >= rhs_lb] & [self.index == index]), + &self.inference_code, + ), )?; context.post( predicate![lhs <= rhs_ub], - conjunction!([self.rhs <= rhs_ub] & [self.index == index]), - &self.inference_code, + ( + conjunction!([self.rhs <= rhs_ub] & [self.index == index]), + &self.inference_code, + ), )?; Ok(()) } @@ -398,154 +405,200 @@ where } } -#[allow(deprecated, reason = "Will be refactored")] #[cfg(test)] mod tests { use pumpkin_checking::TestAtomic; use pumpkin_checking::VariableState; - use pumpkin_core::TestSolver; + use pumpkin_core::predicate; + use pumpkin_core::predicates::Predicate; + use pumpkin_core::predicates::PropositionalConjunction; + use pumpkin_core::propagation::CurrentNogood; + use pumpkin_core::state::State; use super::*; + use crate::StateExt; #[test] fn elements_from_array_with_disjoint_domains_to_rhs_are_filtered_from_index() { - let mut solver = TestSolver::default(); + let mut state = State::default(); - let x_0 = solver.new_variable(4, 6); - let x_1 = solver.new_variable(2, 3); - let x_2 = solver.new_variable(7, 9); - let x_3 = solver.new_variable(14, 15); + let x_0 = state.new_interval_variable(4, 6, None); + let x_1 = state.new_interval_variable(2, 3, None); + let x_2 = state.new_interval_variable(7, 9, None); + let x_3 = state.new_interval_variable(14, 15, None); - let index = solver.new_variable(0, 3); - let rhs = solver.new_variable(6, 9); - let constraint_tag = solver.new_constraint_tag(); + let index = state.new_interval_variable(0, 3, None); + let rhs = state.new_interval_variable(6, 9, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(ElementArgs { - array: vec![x_0, x_1, x_2, x_3].into(), - index, - rhs, - constraint_tag, - }) - .expect("no empty domains"); + let _ = state.add_propagator(ElementArgs { + array: vec![x_0, x_1, x_2, x_3].into(), + index, + rhs, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domains"); - solver.assert_bounds(index, 0, 2); + state.assert_bounds(index, 0, 2); - assert_eq!( - solver.get_reason_int(predicate![index != 3]), - conjunction!([x_3 >= 10] & [rhs <= 9]) + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![index != 3], + &mut reason_buffer, + CurrentNogood::empty(), ); - - assert_eq!( - solver.get_reason_int(predicate![index != 1]), - conjunction!([x_1 <= 5] & [rhs >= 6]) + let reason: PropositionalConjunction = reason_buffer.into(); + assert_eq!(conjunction!([x_3 >= 10] & [rhs <= 9]), reason); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![index != 1], + &mut reason_buffer, + CurrentNogood::empty(), ); + let reason: PropositionalConjunction = reason_buffer.into(); + assert_eq!(conjunction!([x_1 <= 5] & [rhs >= 6]), reason); } #[test] fn bounds_of_rhs_are_min_and_max_of_lower_and_upper_in_array() { - let mut solver = TestSolver::default(); + let mut state = State::default(); - let x_0 = solver.new_variable(3, 10); - let x_1 = solver.new_variable(2, 3); - let x_2 = solver.new_variable(7, 9); - let x_3 = solver.new_variable(14, 15); + let x_0 = state.new_interval_variable(3, 10, None); + let x_1 = state.new_interval_variable(2, 3, None); + let x_2 = state.new_interval_variable(7, 9, None); + let x_3 = state.new_interval_variable(14, 15, None); - let index = solver.new_variable(0, 3); - let rhs = solver.new_variable(0, 20); - let constraint_tag = solver.new_constraint_tag(); + let index = state.new_interval_variable(0, 3, None); + let rhs = state.new_interval_variable(0, 20, None); + let constraint_tag = state.new_constraint_tag(); - let _ = solver - .new_propagator(ElementArgs { - array: vec![x_0, x_1, x_2, x_3].into(), - index, - rhs, - constraint_tag, - }) - .expect("no empty domains"); + let _ = state.add_propagator(ElementArgs { + array: vec![x_0, x_1, x_2, x_3].into(), + index, + rhs, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domains"); - solver.assert_bounds(rhs, 2, 15); + state.assert_bounds(rhs, 2, 15); + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![rhs >= 2], + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!( - solver.get_reason_int(predicate![rhs >= 2]), - conjunction!([x_0 >= 2] & [x_1 >= 2] & [x_2 >= 2] & [x_3 >= 2]) + conjunction!([x_0 >= 2] & [x_1 >= 2] & [x_2 >= 2] & [x_3 >= 2]), + reason ); + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![rhs <= 15], + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!( - solver.get_reason_int(predicate![rhs <= 15]), - conjunction!([x_0 <= 15] & [x_1 <= 15] & [x_2 <= 15] & [x_3 <= 15]) + conjunction!([x_0 <= 15] & [x_1 <= 15] & [x_2 <= 15] & [x_3 <= 15]), + reason ); } #[test] fn fixed_index_propagates_bounds_on_element() { - let mut solver = TestSolver::default(); + let mut state = State::default(); - let x_0 = solver.new_variable(3, 10); - let x_1 = solver.new_variable(0, 15); - let x_2 = solver.new_variable(7, 9); - let x_3 = solver.new_variable(14, 15); - let constraint_tag = solver.new_constraint_tag(); + let x_0 = state.new_interval_variable(3, 10, None); + let x_1 = state.new_interval_variable(0, 15, None); + let x_2 = state.new_interval_variable(7, 9, None); + let x_3 = state.new_interval_variable(14, 15, None); + let constraint_tag = state.new_constraint_tag(); - let index = solver.new_variable(1, 1); - let rhs = solver.new_variable(6, 9); + let index = state.new_interval_variable(1, 1, None); + let rhs = state.new_interval_variable(6, 9, None); - let _ = solver - .new_propagator(ElementArgs { - array: vec![x_0, x_1, x_2, x_3].into(), - index, - rhs, - constraint_tag, - }) - .expect("no empty domains"); + let _ = state.add_propagator(ElementArgs { + array: vec![x_0, x_1, x_2, x_3].into(), + index, + rhs, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domains"); - solver.assert_bounds(x_1, 6, 9); + state.assert_bounds(x_1, 6, 9); - assert_eq!( - solver.get_reason_int(predicate![x_1 >= 6]), - conjunction!([index == 1] & [rhs >= 6]) + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![x_1 >= 6], + &mut reason_buffer, + CurrentNogood::empty(), ); - - assert_eq!( - solver.get_reason_int(predicate![x_1 <= 9]), - conjunction!([index == 1] & [rhs <= 9]) + let reason: PropositionalConjunction = reason_buffer.into(); + assert_eq!(conjunction!([index == 1] & [rhs >= 6]), reason); + + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![x_1 <= 9], + &mut reason_buffer, + CurrentNogood::empty(), ); + let reason: PropositionalConjunction = reason_buffer.into(); + assert_eq!(conjunction!([index == 1] & [rhs <= 9]), reason); } #[test] fn index_hole_propagates_bounds_on_rhs() { - let mut solver = TestSolver::default(); + let mut state = State::default(); - let x_0 = solver.new_variable(3, 10); - let x_1 = solver.new_variable(0, 15); - let x_2 = solver.new_variable(7, 9); - let x_3 = solver.new_variable(14, 15); - let constraint_tag = solver.new_constraint_tag(); + let x_0 = state.new_interval_variable(3, 10, None); + let x_1 = state.new_interval_variable(0, 15, None); + let x_2 = state.new_interval_variable(7, 9, None); + let x_3 = state.new_interval_variable(14, 15, None); + let constraint_tag = state.new_constraint_tag(); - let index = solver.new_variable(0, 3); - solver.remove(index, 1).expect("Value can be removed"); + let index = state.new_interval_variable(0, 3, None); + let _ = state + .post(predicate![index != 1]) + .expect("Value can be removed"); - let rhs = solver.new_variable(-10, 30); + let rhs = state.new_interval_variable(-10, 30, None); - let _ = solver - .new_propagator(ElementArgs { - array: vec![x_0, x_1, x_2, x_3].into(), - index, - rhs, - constraint_tag, - }) - .expect("no empty domains"); + let _ = state.add_propagator(ElementArgs { + array: vec![x_0, x_1, x_2, x_3].into(), + index, + rhs, + constraint_tag, + }); + state.propagate_to_fixed_point().expect("no empty domains"); - solver.assert_bounds(rhs, 3, 15); + state.assert_bounds(rhs, 3, 15); + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![rhs >= 3], + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!( - solver.get_reason_int(predicate![rhs >= 3]), - conjunction!([x_0 >= 3] & [x_2 >= 3] & [x_3 >= 3] & [index != 1]) + conjunction!([x_0 >= 3] & [x_2 >= 3] & [x_3 >= 3] & [index != 1]), + reason ); + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason( + predicate![rhs <= 15], + &mut reason_buffer, + CurrentNogood::empty(), + ); + let reason: PropositionalConjunction = reason_buffer.into(); assert_eq!( - solver.get_reason_int(predicate![rhs <= 15]), - conjunction!([x_0 <= 15] & [x_2 <= 15] & [x_3 <= 15] & [index != 1]) + conjunction!([x_0 <= 15] & [x_2 <= 15] & [x_3 <= 15] & [index != 1]), + reason ); } diff --git a/pumpkin-macros/Cargo.toml b/pumpkin-macros/Cargo.toml index fce5e5a9b..58651bef4 100644 --- a/pumpkin-macros/Cargo.toml +++ b/pumpkin-macros/Cargo.toml @@ -14,10 +14,10 @@ path = "src/lib.rs" proc-macro = true [dependencies] -itertools = "0.13.0" +itertools = "0.14.0" proc-macro2 = "1.0.89" quote = "1.0.37" -stringcase = "0.3.0" +stringcase = "0.4.0" syn = "2.0.85" [lints] diff --git a/pumpkin-proof-processor/Cargo.toml b/pumpkin-proof-processor/Cargo.toml index 0605a2eef..d6233f409 100644 --- a/pumpkin-proof-processor/Cargo.toml +++ b/pumpkin-proof-processor/Cargo.toml @@ -13,7 +13,7 @@ clap-verbosity-flag = "3.0.4" drcp-format = { version = "0.3.1", path = "../drcp-format" } fzn-rs = { version = "0.1.0", path = "../fzn-rs" } flate2 = { version = "1.1.2" } -env_logger = "0.11.8" +env_logger = "0.11.10" pumpkin-core = { version = "0.3.0", path = "../pumpkin-crates/core" } pumpkin-propagators = { version = "0.3.0", path = "../pumpkin-crates/propagators" } log = "0.4.29" diff --git a/pumpkin-proof-processor/src/deduction_propagator.rs b/pumpkin-proof-processor/src/deduction_propagator.rs index 7094ea8b4..bba962e49 100644 --- a/pumpkin-proof-processor/src/deduction_propagator.rs +++ b/pumpkin-proof-processor/src/deduction_propagator.rs @@ -124,7 +124,7 @@ impl Propagator for DeductionPropagator { // This will never fail, as the predicate is known to be unassigned. So // this propagator only returns explicit conflicts and never empty // domain conflicts. - context.post(!unassigned_predicate, explanation, &self.inference_code)?; + context.post(!unassigned_predicate, (explanation, &self.inference_code))?; } } diff --git a/pumpkin-proof-processor/src/processor.rs b/pumpkin-proof-processor/src/processor.rs index 32733a4f4..4c091a96d 100644 --- a/pumpkin-proof-processor/src/processor.rs +++ b/pumpkin-proof-processor/src/processor.rs @@ -487,13 +487,13 @@ impl ProofProcessor { } Conflict::EmptyDomain(empty_domain_confict) => { let mut predicates_to_explain = vec![]; - empty_domain_confict.get_reason( + let maybe_trigger_inference = empty_domain_confict.get_reason( &mut self.state, &mut predicates_to_explain, CurrentNogood::empty(), ); - if let Some(inference_code) = empty_domain_confict.trigger_inference_code { + if let Some(inference_code) = maybe_trigger_inference { let generated_by = inference_code.tag(); let label = inference_code.label(); diff --git a/pumpkin-solver-py/Cargo.toml b/pumpkin-solver-py/Cargo.toml index 70c586f56..897e4772f 100644 --- a/pumpkin-solver-py/Cargo.toml +++ b/pumpkin-solver-py/Cargo.toml @@ -16,10 +16,10 @@ crate-type = ["cdylib"] doc = false [dependencies] -pyo3 = { version = "0.25.1", features= ["extension-module"] } +pyo3 = { version = "0.28.3", features= ["extension-module"] } pumpkin-solver = { version = "0.3.0", path = "../pumpkin-solver" } pumpkin-constraints = { version = "0.3.0", path = "../pumpkin-crates/constraints", features=["clap"] } pumpkin-conflict-resolvers = { version = "0.3.0", path = "../pumpkin-crates/conflict-resolvers/"} [build-dependencies] -pyo3-build-config = "0.25.1" +pyo3-build-config = "0.28.3" diff --git a/pumpkin-solver-py/src/brancher.rs b/pumpkin-solver-py/src/brancher.rs index 0ae40e269..88d2e0437 100644 --- a/pumpkin-solver-py/src/brancher.rs +++ b/pumpkin-solver-py/src/brancher.rs @@ -12,6 +12,7 @@ use pumpkin_solver::core::variables::DomainId; use crate::variables::IntExpression; +#[derive(Debug)] pub struct PythonBrancher { warm_start: WarmStart>, default_brancher: DefaultBrancher, diff --git a/pumpkin-solver-py/src/constraints/globals.rs b/pumpkin-solver-py/src/constraints/globals.rs index f9649c512..75a3a9964 100644 --- a/pumpkin-solver-py/src/constraints/globals.rs +++ b/pumpkin-solver-py/src/constraints/globals.rs @@ -7,7 +7,7 @@ use crate::variables::*; macro_rules! python_constraint { ($name:ident : $constraint_func:ident { $($field:ident : $type:ty),+ $(,)? }) => { - #[pyclass] + #[pyclass(from_py_object)] #[derive(Clone)] pub(crate) struct $name { constraint_tag: Tag, diff --git a/pumpkin-solver-py/src/model.rs b/pumpkin-solver-py/src/model.rs index e35b41880..9137b3682 100644 --- a/pumpkin-solver-py/src/model.rs +++ b/pumpkin-solver-py/src/model.rs @@ -42,7 +42,7 @@ pub struct Model { brancher: PythonBrancher, } -#[pyclass] +#[pyclass(from_py_object)] #[derive(Clone, Debug)] pub struct Tag(pub ConstraintTag); diff --git a/pumpkin-solver-py/src/optimisation.rs b/pumpkin-solver-py/src/optimisation.rs index 429caa3d0..956553a69 100644 --- a/pumpkin-solver-py/src/optimisation.rs +++ b/pumpkin-solver-py/src/optimisation.rs @@ -2,7 +2,8 @@ use pyo3::prelude::*; use crate::result::Solution; -#[pyclass] +#[pyclass(from_py_object)] +#[derive(Clone)] pub enum OptimisationResult { /// The problem was solved to optimality, and the solution is an optimal one. Optimal(Solution), @@ -14,14 +15,14 @@ pub enum OptimisationResult { Unknown(), } -#[pyclass(eq, eq_int)] +#[pyclass(eq, eq_int, from_py_object)] #[derive(Clone, Copy, PartialEq, Eq)] pub enum Optimiser { LinearSatUnsat, LinearUnsatSat, } -#[pyclass(eq, eq_int)] +#[pyclass(eq, eq_int, from_py_object)] #[derive(Clone, Copy, PartialEq, Eq)] pub enum Direction { Minimise, diff --git a/pumpkin-solver-py/src/result.rs b/pumpkin-solver-py/src/result.rs index 3df37942a..b13c782a3 100644 --- a/pumpkin-solver-py/src/result.rs +++ b/pumpkin-solver-py/src/result.rs @@ -5,16 +5,18 @@ use crate::variables::BoolExpression; use crate::variables::IntExpression; use crate::variables::Predicate; -#[pyclass] +#[pyclass(from_py_object)] #[allow(clippy::large_enum_variant)] +#[derive(Clone)] pub enum SatisfactionResult { Satisfiable(Solution), Unsatisfiable(), Unknown(), } -#[pyclass] +#[pyclass(from_py_object)] #[allow(clippy::large_enum_variant)] +#[derive(Clone)] pub enum SatisfactionUnderAssumptionsResult { Satisfiable(Solution), UnsatisfiableUnderAssumptions(Vec), @@ -22,7 +24,7 @@ pub enum SatisfactionUnderAssumptionsResult { Unknown(), } -#[pyclass] +#[pyclass(from_py_object)] #[derive(Clone)] pub struct Solution(pumpkin_solver::core::results::Solution); diff --git a/pumpkin-solver-py/src/variables.rs b/pumpkin-solver-py/src/variables.rs index 6e9e81527..05b15a4ab 100644 --- a/pumpkin-solver-py/src/variables.rs +++ b/pumpkin-solver-py/src/variables.rs @@ -5,7 +5,7 @@ use pumpkin_solver::core::variables::Literal; use pumpkin_solver::core::variables::TransformableVariable; use pyo3::prelude::*; -#[pyclass(eq, hash, frozen)] +#[pyclass(eq, hash, frozen, from_py_object)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct IntExpression(pub AffineView); @@ -26,7 +26,7 @@ impl IntExpression { } } -#[pyclass(eq, hash, frozen)] +#[pyclass(eq, hash, frozen, from_py_object)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct BoolExpression(pub Literal); @@ -47,7 +47,7 @@ impl From for BoolExpression { } } -#[pyclass(eq, eq_int, hash, frozen)] +#[pyclass(eq, eq_int, hash, frozen, from_py_object)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Comparator { NotEqual, @@ -56,7 +56,7 @@ pub enum Comparator { GreaterThanOrEqual, } -#[pyclass(eq, get_all, hash, frozen)] +#[pyclass(eq, get_all, hash, frozen, from_py_object)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct Predicate { pub variable: IntExpression, diff --git a/pumpkin-solver/Cargo.toml b/pumpkin-solver/Cargo.toml index 495d4cb57..0e17e552a 100644 --- a/pumpkin-solver/Cargo.toml +++ b/pumpkin-solver/Cargo.toml @@ -10,24 +10,26 @@ repository.workspace = true [dependencies] clap = { version = "4.5.17", features = ["derive"] } -env_logger = "0.10.0" +clap-verbosity-flag = "3.0.4" +env_logger = "0.11.10" flatzinc = "0.3.21" log = "0.4.27" pumpkin-core = { version = "0.3.0", path = "../pumpkin-crates/core/", features = ["clap"] } pumpkin-constraints = { version = "0.3.0", path = "../pumpkin-crates/constraints/"} pumpkin-propagators = { version = "0.3.0", path = "../pumpkin-crates/propagators/", features=["clap"]} pumpkin-conflict-resolvers = { version = "0.3.0", path = "../pumpkin-crates/conflict-resolvers/"} -signal-hook = "0.3.18" +signal-hook = "0.4.4" thiserror = "2.0.12" [dev-dependencies] clap = { version = "4.5.17", features = ["derive"] } -env_logger = "0.10.0" +env_logger = "0.11.10" regex = "1.11.0" -stringcase = "0.3.0" +stringcase = "0.4.0" wait-timeout = "0.2.0" pumpkin-macros = { path = "../pumpkin-macros"} pumpkin-checker = { path = "../pumpkin-checker"} +paste = "1.0.15" [lints] workspace = true @@ -37,4 +39,4 @@ debug-checks = ["pumpkin-core/debug-checks"] check-propagations = ["pumpkin-core/check-propagations"] [build-dependencies] -cc = "1.1.30" +cc = "1.2.61" diff --git a/pumpkin-solver/build.rs b/pumpkin-solver/build.rs index 54c8802f8..6fc921bfd 100644 --- a/pumpkin-solver/build.rs +++ b/pumpkin-solver/build.rs @@ -10,6 +10,8 @@ fn main() { } fn run() -> Result<(), Box> { + determine_git_hash(); + println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-env-changed=NO_CHECKERS"); @@ -34,6 +36,24 @@ fn run() -> Result<(), Box> { Ok(()) } +fn determine_git_hash() { + let output = Command::new("git") + .args(["rev-parse", "HEAD"]) + .output() + .expect("Failed to execute git command"); + + let git_sha = String::from_utf8(output.stdout) + .expect("Git output was not valid UTF-8") + .trim() + .to_string(); + + println!("cargo:rustc-env=GIT_SHA={}", git_sha); + + // Rerun if HEAD changes (new commits, branch switches, etc.) + println!("cargo:rerun-if-changed=.git/HEAD"); + println!("cargo:rerun-if-changed=.git/refs"); +} + fn compile_c_binary>( sources: &[Source], output_stem: &str, diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs index 52d1fc609..cbee4da29 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/create_search_strategy.rs @@ -210,7 +210,9 @@ fn create_from_search_strategy( )), None => {} } - brancher.add_brancher(Box::new(context.solver.default_brancher())); + + // brancher.add_brancher(Box::new(context.solver.default_brancher())); + brancher.add_brancher(Box::new(context.solver.backup_brancher())); } Ok(brancher) diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs index e72963c14..1f7d35ce1 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/compiler/post_constraints.rs @@ -1,5 +1,6 @@ //! Compile constraints into CP propagators +use std::num::NonZero; use std::rc::Rc; use pumpkin_propagators::disjunctive::ArgDisjunctiveTask; @@ -18,6 +19,18 @@ use crate::flatzinc::FlatZincOptions; use crate::flatzinc::ast::FlatZincAst; use crate::flatzinc::compiler::context::Set; +macro_rules! check_parameters { + ($exprs:ident, $num_parameters:expr, $name:expr) => { + if $exprs.len() != $num_parameters { + return Err(FlatZincError::IncorrectNumberOfArguments { + constraint_id: $name.into(), + expected: $num_parameters, + actual: $exprs.len(), + }); + } + }; +} + pub(crate) fn run( _: &FlatZincAst, context: &mut CompilationContext, @@ -104,46 +117,6 @@ pub(crate) fn run( pumpkin_constraints::binary_not_equals, )?, - "int_lin_eq_imp" => compile_int_lin_imp_predicate( - context, - exprs, - annos, - "int_lin_eq_imp", - constraint_tag, - pumpkin_constraints::equals, - )?, - "int_lin_ge_imp" => compile_int_lin_imp_predicate( - context, - exprs, - annos, - "int_lin_ge_imp", - constraint_tag, - pumpkin_constraints::greater_than_or_equals, - )?, - "int_lin_gt_imp" => compile_int_lin_imp_predicate( - context, - exprs, - annos, - "int_lin_gt_imp", - constraint_tag, - pumpkin_constraints::greater_than, - )?, - "int_lin_le_imp" => compile_int_lin_imp_predicate( - context, - exprs, - annos, - "int_lin_le_imp", - constraint_tag, - pumpkin_constraints::less_than_or_equals, - )?, - "int_lin_lt_imp" => compile_int_lin_imp_predicate( - context, - exprs, - annos, - "int_lin_lt_imp", - constraint_tag, - pumpkin_constraints::less_than, - )?, "int_lin_ne_imp" => compile_int_lin_imp_predicate( context, exprs, @@ -169,38 +142,132 @@ pub(crate) fn run( constraint_tag, pumpkin_constraints::not_equals, )?, - "int_lin_le" => compile_int_lin_predicate( - context, - exprs, - annos, - "int_lin_le", - constraint_tag, - pumpkin_constraints::less_than_or_equals, - )?, - "int_lin_le_reif" => compile_reified_int_lin_predicate( - context, - exprs, - annos, - "int_lin_le_reif", - constraint_tag, - pumpkin_constraints::less_than_or_equals, - )?, - "int_lin_eq" => compile_int_lin_predicate( - context, - exprs, - annos, - "int_lin_eq", - constraint_tag, - pumpkin_constraints::equals, - )?, - "int_lin_eq_reif" => compile_reified_int_lin_predicate( - context, - exprs, - annos, - "int_lin_eq_reif", - constraint_tag, - pumpkin_constraints::equals, - )?, + "int_lin_le" => { + if !options.use_hypercube_linear { + compile_int_lin_predicate( + context, + exprs, + annos, + "int_lin_le", + constraint_tag, + pumpkin_constraints::less_than_or_equals, + )? + } else { + compile_int_lin_predicate( + context, + exprs, + annos, + "int_lin_le", + constraint_tag, + hl_for_lin_le, + )? + } + } + "int_lin_le_imp" => { + if !options.use_hypercube_linear { + compile_int_lin_imp_predicate( + context, + exprs, + annos, + "int_lin_le_imp", + constraint_tag, + pumpkin_constraints::less_than_or_equals, + )? + } else { + compile_int_lin_imp_predicate( + context, + exprs, + annos, + "int_lin_le_imp", + constraint_tag, + hl_for_lin_le, + )? + } + } + "int_lin_le_reif" => { + if !options.use_hypercube_linear { + compile_reified_int_lin_predicate( + context, + exprs, + annos, + "int_lin_le_reif", + constraint_tag, + pumpkin_constraints::less_than_or_equals, + )? + } else { + compile_reified_int_lin_predicate( + context, + exprs, + annos, + "int_lin_le_reif", + constraint_tag, + hl_for_lin_le, + )? + } + } + "int_lin_eq" => { + if !options.use_hypercube_linear { + compile_int_lin_predicate( + context, + exprs, + annos, + "int_lin_eq", + constraint_tag, + pumpkin_constraints::equals, + )? + } else { + compile_int_lin_predicate( + context, + exprs, + annos, + "int_lin_eq", + constraint_tag, + hl_for_lin_eq, + )? + } + } + "int_lin_eq_imp" => { + if !options.use_hypercube_linear { + compile_int_lin_imp_predicate( + context, + exprs, + annos, + "int_lin_eq_imp", + constraint_tag, + pumpkin_constraints::equals, + )? + } else { + compile_int_lin_imp_predicate( + context, + exprs, + annos, + "int_lin_eq_imp", + constraint_tag, + hl_for_lin_eq, + )? + } + } + "int_lin_eq_reif" => { + if !options.use_hypercube_linear { + compile_reified_int_lin_predicate( + context, + exprs, + annos, + "int_lin_eq_reif", + constraint_tag, + pumpkin_constraints::equals, + )? + } else { + compile_reified_int_lin_predicate( + context, + exprs, + annos, + "int_lin_eq_reif", + constraint_tag, + hl_for_lin_eq, + )? + } + } "int_ne" => compile_binary_int_predicate( context, exprs, @@ -356,18 +423,6 @@ pub(crate) fn run( Ok(()) } -macro_rules! check_parameters { - ($exprs:ident, $num_parameters:expr, $name:expr) => { - if $exprs.len() != $num_parameters { - return Err(FlatZincError::IncorrectNumberOfArguments { - constraint_id: $name.into(), - expected: $num_parameters, - actual: $exprs.len(), - }); - } - }; -} - fn compile_disjunctive_strict( context: &mut CompilationContext<'_>, exprs: &[flatzinc::Expr], @@ -1014,3 +1069,41 @@ fn create_table(flat_table: Rc<[i32]>, num_variables: usize) -> Vec> { table } + +fn hl_for_lin_le( + terms: Box<[AffineView]>, + rhs: i32, + constraint_tag: ConstraintTag, +) -> impl NegatableConstraint { + pumpkin_core::hypercube_linear::hypercube_linear_le( + std::iter::empty(), + terms.into_iter().map(|view| { + assert_eq!(view.offset, 0); + + let weight = NonZero::new(view.scale).expect("zero weight in int_lin_le_imp"); + + (weight, view.inner) + }), + rhs, + constraint_tag, + ) +} + +fn hl_for_lin_eq( + terms: Box<[AffineView]>, + rhs: i32, + constraint_tag: ConstraintTag, +) -> impl NegatableConstraint { + pumpkin_core::hypercube_linear::hypercube_linear_eq( + std::iter::empty(), + terms.into_iter().map(|view| { + assert_eq!(view.offset, 0); + + let weight = NonZero::new(view.scale).expect("zero weight in int_lin_le_imp"); + + (weight, view.inner) + }), + rhs, + constraint_tag, + ) +} diff --git a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs index eae152942..d86424a08 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/flatzinc/mod.rs @@ -76,6 +76,9 @@ pub(crate) struct FlatZincOptions { /// Indicates that the solver should perform verbose logging pub(crate) verbose: bool, + + /// Whether to use hypercube linear propagator when possible. + pub(crate) use_hypercube_linear: bool, } fn log_statistics( diff --git a/pumpkin-solver/src/bin/pumpkin-solver/main.rs b/pumpkin-solver/src/bin/pumpkin-solver/main.rs index 1edc75199..48a4085d1 100644 --- a/pumpkin-solver/src/bin/pumpkin-solver/main.rs +++ b/pumpkin-solver/src/bin/pumpkin-solver/main.rs @@ -14,6 +14,7 @@ use std::time::Duration; use clap::Parser; use clap::ValueEnum; +use clap_verbosity_flag::Verbosity; use file_format::FileFormat; use log::Level; use log::LevelFilter; @@ -27,6 +28,9 @@ use parsers::dimacs::parse_cnf; use pumpkin_conflict_resolvers::resolvers::AnalysisMode; use pumpkin_conflict_resolvers::resolvers::NoLearningResolver; use pumpkin_conflict_resolvers::resolvers::ResolutionResolver; +use pumpkin_core::hypercube_linear::HypercubeLinearResolver; +use pumpkin_core::hypercube_linear::Trace; +use pumpkin_core::hypercube_linear::TraceOptions; use pumpkin_propagators::cumulative::options::CumulativeOptions; use pumpkin_propagators::cumulative::options::CumulativePropagationMethod; use pumpkin_propagators::cumulative::time_table::CumulativeExplanationType; @@ -289,15 +293,9 @@ struct Args { )] random_seed: u64, - /// Enables log message output from the solver. - /// - /// For printing statistics see the option "--log-statistics", and for printing all solutions - /// (in case of a satisfaction problem) or printing solutions of increasing quality (in case of - /// an optimization problem) see the option "--all-solutions". - /// - /// Possible values: bool - #[arg(short = 'v', long = "verbose", verbatim_doc_comment)] - verbose: bool, + /// Set the verbosity of the solver logs. + #[command(flatten)] + verbose: Verbosity, /// Enables logging of statistics from the solver. /// @@ -401,20 +399,32 @@ struct Args { /// The amount of memory (in MB) that is preallocated for storing nogoods. #[arg(long = "memory-preallocated", default_value_t = 50)] memory_preallocated: usize, + + /// Include intermediate steps in the hypercube linear proof. + /// + /// Ignored if not using hypercube linear resolver or not logging a proof. + #[arg(long = "hl-proof-with-intermediates", default_value_t)] + hl_proof_with_intermediates: bool, + + /// Use the middling propositional resolution operator. + /// + /// Ignored if not using a hypercube linear resolver. + #[arg(long = "hl-middling-propres", default_value_t)] + hl_middling_propres: bool, } fn configure_logging( file_format: FileFormat, - verbose: bool, + verbosity: Verbosity, log_statistics: bool, omit_timestamp: bool, omit_call_site: bool, ) -> std::io::Result<()> { match file_format { FileFormat::CnfDimacsPLine | FileFormat::WcnfDimacsPLine => { - configure_logging_sat(verbose, log_statistics, omit_timestamp, omit_call_site) + configure_logging_sat(verbosity, log_statistics, omit_timestamp, omit_call_site) } - FileFormat::FlatZinc => configure_logging_minizinc(verbose, log_statistics), + FileFormat::FlatZinc => configure_logging_minizinc(verbosity, log_statistics), } } @@ -427,7 +437,7 @@ fn configure_logging_unknown() -> std::io::Result<()> { Ok(()) } -fn configure_logging_minizinc(verbose: bool, log_statistics: bool) -> std::io::Result<()> { +fn configure_logging_minizinc(verbosity: Verbosity, log_statistics: bool) -> std::io::Result<()> { if log_statistics { configure_statistic_logging( "%%%mzn-stat:", @@ -436,11 +446,7 @@ fn configure_logging_minizinc(verbose: bool, log_statistics: bool) -> std::io::R None, ); } - let level_filter = if verbose { - LevelFilter::Debug - } else { - LevelFilter::Warn - }; + let level_filter = verbosity.log_level_filter(); env_logger::Builder::new() .format(move |buf, record| { @@ -456,7 +462,7 @@ fn configure_logging_minizinc(verbose: bool, log_statistics: bool) -> std::io::R } fn configure_logging_sat( - verbose: bool, + verbosity: Verbosity, log_statistics: bool, omit_timestamp: bool, omit_call_site: bool, @@ -464,11 +470,7 @@ fn configure_logging_sat( if log_statistics { configure_statistic_logging("c STAT", None, None, None); } - let level_filter = if verbose { - LevelFilter::Debug - } else { - LevelFilter::Warn - }; + let level_filter = verbosity.log_level_filter(); env_logger::Builder::new() .format(move |buf, record| { @@ -534,7 +536,9 @@ fn run() -> PumpkinResult<()> { ); }; - let proof_log = if let Some(path_buf) = args.proof_path.as_ref() { + let proof_log = if args.conflict_resolver == ConflictResolverType::UIP + && let Some(path_buf) = args.proof_path.as_ref() + { match file_format { FileFormat::CnfDimacsPLine => ProofLog::dimacs(path_buf)?, FileFormat::WcnfDimacsPLine => { @@ -567,20 +571,15 @@ fn run() -> PumpkinResult<()> { activity_bump_increment: 1.0, }; - let should_minimise_nogoods = if args.proof_type == ProofType::Full { - warn!("Recursive minimisation is disabled when logging the full proof."); - false - } else { - !args.no_learning_clause_minimisation - }; let solver_options = SolverOptions { // 1 MB is 1_000_000 bytes memory_preallocated: args.memory_preallocated, restart_options, - should_minimise_nogoods, + should_minimise_nogoods: !args.no_learning_clause_minimisation, random_generator: SmallRng::seed_from_u64(args.random_seed), proof_log, learning_options, + resolver_type: args.conflict_resolver, }; let time_limit = args.time_limit.map(Duration::from_millis); @@ -614,7 +613,8 @@ fn run() -> PumpkinResult<()> { ), optimisation_strategy: args.optimisation_strategy, proof_type: args.proof_path.map(|_| args.proof_type), - verbose: args.verbose, + verbose: args.verbose.log_level_filter() >= LevelFilter::Info, + use_hypercube_linear: false, }, NoLearningResolver, )?, @@ -634,10 +634,58 @@ fn run() -> PumpkinResult<()> { ), optimisation_strategy: args.optimisation_strategy, proof_type: args.proof_path.map(|_| args.proof_type), - verbose: args.verbose, + verbose: args.verbose.log_level_filter() >= LevelFilter::Info, + use_hypercube_linear: false, }, - ResolutionResolver::new(AnalysisMode::OneUIP, should_minimise_nogoods), + ResolutionResolver::new( + AnalysisMode::OneUIP, + !args.no_learning_clause_minimisation, + ), )?, + ConflictResolverType::HypercubeLinear => { + let trace = args + .proof_path + .as_ref() + .map(File::create) + .transpose()? + .map(|file| { + Trace::to_file( + file, + TraceOptions { + include_intermediate_steps: args.hl_proof_with_intermediates, + }, + ) + }) + .unwrap_or(Trace::discard()); + + let resolver = if args.hl_middling_propres { + HypercubeLinearResolver::with_middling_resh(trace) + } else { + HypercubeLinearResolver::new(trace) + }; + + flatzinc::solve( + Solver::with_options(solver_options), + instance_path, + time_limit, + FlatZincOptions { + free_search: args.free_search, + all_solutions: args.all_solutions, + cumulative_options: CumulativeOptions::new( + args.cumulative_allow_holes, + args.cumulative_explanation_type, + !args.cumulative_single_profiles, + args.cumulative_propagation_method, + args.cumulative_incremental_backtracking, + ), + optimisation_strategy: args.optimisation_strategy, + proof_type: args.proof_path.as_ref().map(|_| args.proof_type), + verbose: args.verbose.log_level_filter() >= LevelFilter::Info, + use_hypercube_linear: true, + }, + resolver, + )? + } }, } diff --git a/pumpkin-solver/src/lib.rs b/pumpkin-solver/src/lib.rs index fbf3dc479..cfc776f4b 100644 --- a/pumpkin-solver/src/lib.rs +++ b/pumpkin-solver/src/lib.rs @@ -356,3 +356,9 @@ pub use pumpkin_constraints::*; pub use pumpkin_core::Solver; #[cfg(doc)] use pumpkin_core::conflict_resolving::ConflictResolver; + +/// The version of Pumpkin that is being executed. +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// The latest commit hash. +pub const LATEST_COMMIT: &str = env!("GIT_SHA"); diff --git a/pumpkin-solver/tests/helpers/mod.rs b/pumpkin-solver/tests/helpers/mod.rs index cff79aa31..9fa620e99 100644 --- a/pumpkin-solver/tests/helpers/mod.rs +++ b/pumpkin-solver/tests/helpers/mod.rs @@ -57,7 +57,7 @@ pub(crate) fn run_solver_with_options( let solver = PathBuf::from(env!("CARGO_BIN_EXE_pumpkin-solver")); let add_extension = |extension: &str| -> PathBuf { - if let Some(prefix) = prefix.and_then(|s| if s.is_empty() { None } else { Some(s) }) { + if let Some(prefix) = prefix.filter(|s| !s.is_empty()) { instance_path.with_extension(format!("{prefix}.{extension}")) } else { instance_path.with_extension(extension) diff --git a/pumpkin-solver/tests/mzn_constraint_test.rs b/pumpkin-solver/tests/mzn_constraint_test.rs index 635c7a1ab..ad70ece49 100644 --- a/pumpkin-solver/tests/mzn_constraint_test.rs +++ b/pumpkin-solver/tests/mzn_constraint_test.rs @@ -15,12 +15,30 @@ macro_rules! mzn_test { ($name:ident, $file:expr, $options:expr) => { #[test] fn $name() { + let mut actual_options = vec![]; + actual_options.extend($options); + + let output = run_mzn_test_with_options::( + $file, + "mzn_constraints", + false, + TestType::SolutionEnumeration, + actual_options.clone(), + stringify!($name), + ); + assert!(output.ends_with("==========\n")); + + actual_options.extend([ + "--conflict-resolver".to_owned(), + "hypercube-linear".to_owned(), + ]); + let output = run_mzn_test_with_options::( $file, "mzn_constraints", false, TestType::SolutionEnumeration, - $options, + actual_options, stringify!($name), ); assert!(output.ends_with("==========\n")); diff --git a/pumpkin-solver/tests/mzn_optimization/minimise_1.expected b/pumpkin-solver/tests/mzn_optimization/minimise_1.expected index 5fd2daba9..d86b82442 100644 --- a/pumpkin-solver/tests/mzn_optimization/minimise_1.expected +++ b/pumpkin-solver/tests/mzn_optimization/minimise_1.expected @@ -1,5 +1,5 @@ -x = 5; -y = 5; +x = 7; +y = 3; z = 7; objective = 7; ---------- diff --git a/pumpkin-solver/tests/wcnf_test.rs b/pumpkin-solver/tests/wcnf_test.rs index b5a057330..1338ef7e2 100644 --- a/pumpkin-solver/tests/wcnf_test.rs +++ b/pumpkin-solver/tests/wcnf_test.rs @@ -28,6 +28,7 @@ test_wcnf_instance!(johnson8_4_4, 56); test_wcnf_instance!(normalized_g2x2, 2); test_wcnf_instance!(normalized_g9x3, 7); // test_wcnf_instance!(normalized_g9x9, 20); +#[cfg(not(feature = "check-propagations"))] test_wcnf_instance!(ram_k3_n9, 1); struct MaxSATChecker {