diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 63928ab40..502e837f5 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -25,7 +25,7 @@ These repo-local skills live under `.claude/skills/*/SKILL.md`. - [review-quality](skills/review-quality/SKILL.md) -- Generic code quality review: DRY, KISS, cohesion/coupling, test quality, HCI. Read-only, no code changes. Called by `review-pipeline`. - [fix-pr](skills/fix-pr/SKILL.md) -- Resolve PR review comments, fix CI failures, and address codecov coverage gaps. Uses `gh api` for codecov (not local `cargo-llvm-cov`). - [write-model-in-paper](skills/write-model-in-paper/SKILL.md) -- Write or improve a problem-def entry in the Typst paper (standalone, for improving existing entries). Core instructions are inlined in `add-model` Step 6. -- [write-rule-in-paper](skills/write-rule-in-paper/SKILL.md) -- Write or improve a reduction-rule entry in the Typst paper (standalone, for improving existing entries). Core instructions are inlined in `add-rule` Step 5. +- [write-rule-in-paper](skills/write-rule-in-paper/SKILL.md) -- Write or improve a reduction-rule entry in the Typst paper (standalone, for improving existing entries). Core instructions are inlined in `add-rule` Step 6. - [release](skills/release/SKILL.md) -- Create a new crate release. Determines version bump from diff, verifies tests/clippy, then runs `make release`. - [check-issue](skills/check-issue/SKILL.md) -- Quality gate for `[Rule]` and `[Model]` issues. Checks usefulness, non-triviality, correctness of literature, and writing quality. Posts structured report and adds failure labels. - [fix-issue](skills/fix-issue/SKILL.md) -- Fix quality issues found by check-issue — auto-fixes mechanical problems, brainstorms substantive issues with human, then re-checks and moves to Ready. @@ -59,7 +59,7 @@ make fmt-check # Check code formatting make clippy # Run clippy lints make doc # Build mdBook documentation (includes reduction graph export) make mdbook # Build and serve mdBook with live reload -make paper # Build Typst paper from checked-in example fixtures +make paper # Generate example data and build the Typst paper make coverage # Generate coverage report (>95% required) make check # Quick pre-commit check (fmt + clippy + test) make rust-export # Generate Julia parity test data (mapping stages) @@ -158,6 +158,8 @@ Max, Min, Sum, Or, And, Extremum, ExtremumSense - `Solver::solve()` computes the aggregate value for any `Problem` whose `Value` implements `Aggregate` - `BruteForce::find_witness()` / `find_all_witnesses()` recover witnesses only when `P::Value::supports_witnesses()` - `ReductionResult` provides `target_problem()` and `extract_solution()` for witness/config workflows; `AggregateReductionResult` provides `extract_value()` for aggregate/value workflows +- Every direct `extract_solution()` must call `validate_target_solution()` once before decoding; composed extractors delegate validation to the first direct decoder. +- Decode only the reduction's defined mathematical mapping. Reject malformed structure with `ExtractionError`; never panic, truncate, clamp, invent defaults, or add recovery branches. Explicit mathematical alternatives and sentinels are allowed. Test successful decoding and every rejected representation. - CLI-facing dynamic formatting uses aggregate wrapper names directly (for example `Max(2)`, `Min(None)`, `Or(true)`, or `Sum(56)`) - Graph types: SimpleGraph, PlanarGraph, BipartiteGraph, UnitDiskGraph, KingsSubgraph, TriangularSubgraph - Weight types: `One` (unit weight marker), `i32`, `f64` — all implement `WeightElement` trait @@ -208,14 +210,14 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: - Aggregate-only models are first-class in `declare_variants!`; aggregate-only and Turing reduction edges still need manual `ReductionEntry` wiring because `#[reduction]` only registers witness/config reductions today - Exact registry dispatch lives in `src/registry/`; alias resolution and partial/default variant resolution live in `problemreductions-cli/src/problem_name.rs` - `pred create` schema-driven dispatch lives in `problemreductions-cli/src/commands/create.rs` (`create_schema_driven()`) -- Canonical paper and CLI examples live in `src/example_db/model_builders.rs` and `src/example_db/rule_builders.rs` +- Canonical model examples live in `src/example_db/model_builders.rs`; rule examples live beside their rules and are collected by `src/rules/mod.rs` ## Conventions ### File Naming - Reduction files: `src/rules/_.rs` (e.g., `maximumindependentset_qubo.rs`) - Model files: `src/models//.rs` — category is by input structure: `graph/` (graph input), `formula/` (boolean formula/circuit), `set/` (universe + subsets), `algebraic/` (matrix/linear system/lattice), `misc/` (other) -- Canonical examples: builder functions in `src/example_db/rule_builders.rs` and `src/example_db/model_builders.rs` +- Canonical examples: model builders in `src/example_db/model_builders.rs`; rule-local `canonical_rule_example_specs()` functions collected by `src/rules/mod.rs` - Example binaries in `examples/`: utility/export tools and pedagogical demos only (not per-reduction files) - Test naming: `test__to__closed_loop` @@ -261,7 +263,7 @@ Model review automation checks for a dedicated test file under `src/unit_tests/m - `.claude/` — Claude Code instructions and skills - `docs/book/` — mdBook user documentation (built with `make doc`) - `docs/paper/reductions.typ` — Typst paper with problem definitions and reduction theorems -- `src/example_db/` — Canonical model/rule examples: `model_builders.rs`, `rule_builders.rs` (in-memory builders), `specs.rs` (per-module invariant specs), consumed by `pred create --example` and paper exports +- `src/example_db/` — Model builders, shared example specs, and rule-example aggregation consumed by `pred create --example` and paper exports - `examples/` — Export utilities, graph-analysis helpers, and pedagogical demos ## Documentation Requirements diff --git a/.claude/skills/add-rule/SKILL.md b/.claude/skills/add-rule/SKILL.md index 33a303af6..5e9b01ea6 100644 --- a/.claude/skills/add-rule/SKILL.md +++ b/.claude/skills/add-rule/SKILL.md @@ -106,13 +106,19 @@ impl ReductionResult for ReductionXToY { type Source = SourceType; type Target = TargetType; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution(&self, target_solution: &[usize]) -> Vec { - // Map target solution back to source solution - // If Step 1 ran: translate the verified Python extract_solution() logic + fn extract_solution( + &self, + target_solution: &[usize], + ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + let source_solution = /* translate the verified mathematical mapping exactly */; + Ok(source_solution) } } ``` +Every direct extractor must call `validate_target_solution()` once before decoding. It checks only length and value domains, not feasibility, optimality, or rule-specific structure; reject malformed structure with `ExtractionError`. + **ReduceTo with `#[reduction]` macro** (overhead is **required**): ```rust #[reduction(overhead = { @@ -156,6 +162,8 @@ Additional recommended tests: - Edge cases (empty graph, single vertex, etc.) - Weight preservation (if applicable) +Test every malformed representation distinguished by the decoder (for example, zero or multiple one-hot selections, or duplicate permutation entries). The canonical example supplies shared wrong-length and out-of-domain tests. + For aggregate-only reductions, replace the closed-loop witness test with value-chain tests: - Solve the target with `Solver::solve()` - Map the aggregate value back with `extract_value()` @@ -163,9 +171,9 @@ For aggregate-only reductions, replace the closed-loop witness test with value-c Link via `#[cfg(test)] #[path = "..."] mod tests;` at the bottom of the rule file. -## Step 5: Add canonical example to example_db +## Step 5: Add canonical example -Add a builder function in `src/example_db/rule_builders.rs` that constructs a small, canonical instance for this reduction. Follow the existing patterns in that file. Register the builder in `build_rule_examples()`. +Define `canonical_rule_example_specs()` in the rule module and include it from `src/rules/mod.rs::canonical_rule_example_specs()`. This enrolls the rule in shared round-trip, wrong-length, and out-of-domain extraction tests. ## Step 6: Document in paper (MANDATORY — DO NOT SKIP) @@ -231,11 +239,11 @@ Checklist: notation self-contained, complexity cited, overhead consistent, examp ```bash cargo run --example export_graph # Generate reduction_graph.json for docs/paper builds cargo run --example export_schemas # Generate problem schemas for docs/paper builds -make regenerate-fixtures # Regenerate example_db/fixtures/examples.json (slow, needs ILP) +cargo run --features "example-db" --example export_examples make test clippy # Must pass ``` -`make regenerate-fixtures` is required so the paper can load the new rule's example data from `src/example_db/fixtures/examples.json`. Without it, the `reduction-rule` entry in Step 6 will reference missing fixture data. +`export_examples` refreshes the gitignored `docs/paper/data/examples.json` used by the paper. Structural and quality review is handled by the `review-pipeline` stage, not here. The run stage just needs to produce working code. @@ -251,6 +259,8 @@ Structural and quality review is handled by the `review-pipeline` stage, not her Adding a witness-preserving reduction rule does NOT require CLI changes -- the reduction graph is auto-generated from `#[reduction]` macros and the CLI discovers paths dynamically. However, both source and target models must already be fully registered through their model files (`declare_variants!`), aliases as needed in `problem_name.rs`, and `pred create` support where applicable (see `add-model` skill). +`ExtractionError` already propagates through `pred extract` and bundle `pred solve`; add a rule-specific CLI test only when the CLI surface changes. + Aggregate-only reductions currently have a narrower CLI surface: - `pred solve ` can still compute direct aggregate values for aggregate-only problems - `pred reduce` and `pred solve bundle.json` remain witness-only workflows and reject aggregate-only paths @@ -261,7 +271,7 @@ Aggregate-only reductions currently have a narrower CLI surface: - Rule file: `src/rules/_.rs` -- no underscores within a problem name - e.g., `maximumindependentset_qubo.rs`, `minimumvertexcover_maximumindependentset.rs` - Test file: `src/unit_tests/rules/_.rs` -- Canonical example: builder function in `src/example_db/rule_builders.rs` +- Canonical example: `canonical_rule_example_specs()` in the rule module, included from `src/rules/mod.rs` ## Common Mistakes @@ -272,9 +282,10 @@ Aggregate-only reductions currently have a narrower CLI surface: | Wrong overhead expression | Must accurately reflect the size relationship | | Adding extra reduction metadata or duplicate primitive endpoint registration | Keep one primitive registration per endpoint pair and use only the `overhead` form of `#[reduction]` | | Missing `extract_solution` mapping state | Store any index maps needed in the ReductionResult struct | -| Not adding canonical example to `example_db` | Add builder in `src/example_db/rule_builders.rs` | +| Permissive extraction | Validate first, then map exactly or return `ExtractionError` | +| Not adding a canonical example | Add the rule-local spec and include it from `src/rules/mod.rs` | | Not regenerating reduction graph | Run `cargo run --example export_graph` after adding a rule | -| Skipping Step 5 (paper documentation) | **Every rule MUST have a `reduction-rule` entry in the paper. This is mandatory, not optional. PRs without documentation will be rejected.** | +| Skipping Step 6 (paper documentation) | **Every rule MUST have a `reduction-rule` entry in the paper. This is mandatory, not optional. PRs without documentation will be rejected.** | | Source/target model not fully registered | Both problems must already have `declare_variants!`, aliases as needed, and CLI create support -- use `add-model` skill first | | Treating a direct-to-ILP rule as a toy stub | Direct ILP reductions need exact overhead metadata and strong semantic regression tests, just like other production ILP rules | | Skipping verification for complex reductions | Verification is default for a reason — `--no-verify` is for trivial identity/complement reductions only | diff --git a/.claude/skills/final-review/SKILL.md b/.claude/skills/final-review/SKILL.md index 633d86ad8..ced146b7d 100644 --- a/.claude/skills/final-review/SKILL.md +++ b/.claude/skills/final-review/SKILL.md @@ -168,12 +168,12 @@ Use `AskUserQuestion` with your recommendation: Scan the PR diff for dangerous actions: -- **Blacklisted files**: If the diff touches `docs/src/reductions/reduction_graph.json`, `docs/src/reductions/problem_schemas.json`, or `src/example_db/fixtures/examples.json` (legacy, no longer exists), **block merge**. These files are auto-generated and must not be committed in PRs — they are rebuilt by CI/`make doc`/`make paper`. Flag immediately and recommend OnHold. +- **Blacklisted files**: If the diff touches `docs/src/reductions/reduction_graph.json` or `docs/src/reductions/problem_schemas.json`, **block merge**. These files are auto-generated and must not be committed in PRs — they are rebuilt by CI/`make doc`/`make paper`. Flag immediately and recommend OnHold. - **Removed features**: Any existing model, rule, test, or example deleted? - **Unrelated changes**: Files modified that don't belong to this PR (e.g., changes to unrelated models/rules, CI config, Cargo.toml dependency changes not needed for this PR) - **Force push indicators**: Any sign of history rewriting - **Broad modifications**: Changes to core traits, macros, or shared infrastructure that could affect other features -- **No committed `examples.json`**: The example database is generated on demand by `make paper` (via `export_examples`). PRs should not commit `src/example_db/fixtures/examples.json` (legacy path, deleted) or `docs/paper/data/examples.json` (current output path) — both are gitignored build artifacts. +- **No committed `examples.json`**: The example database is generated on demand by `make paper` (via `export_examples`). Do not commit the gitignored `docs/paper/data/examples.json` build artifact. Report findings with fix options for each concern: diff --git a/.claude/skills/issue-to-pr/SKILL.md b/.claude/skills/issue-to-pr/SKILL.md index 2573bedd5..87734784e 100644 --- a/.claude/skills/issue-to-pr/SKILL.md +++ b/.claude/skills/issue-to-pr/SKILL.md @@ -92,12 +92,12 @@ Write implementation plan to `docs/plans/YYYY-MM-DD-.md` using `superpower The plan MUST reference the appropriate implementation skill and follow its steps: - **For ordinary `[Model]` issues:** Follow [add-model](../add-model/SKILL.md) Steps 1-7 as the action pipeline -- **For `[Model]` issues that explicitly claim direct ILP solving:** Follow [add-model](../add-model/SKILL.md) Steps 1-7 **and** [add-rule](../add-rule/SKILL.md) Steps 1-6 for the direct ` -> ILP` rule in the same plan / PR +- **For `[Model]` issues that explicitly claim direct ILP solving:** Follow [add-model](../add-model/SKILL.md) Steps 1-7 **and** [add-rule](../add-rule/SKILL.md) Steps 1-7 for the direct ` -> ILP` rule in the same plan / PR - **For `[Rule]` issues:** Follow [add-rule](../add-rule/SKILL.md) Steps 1-7 as the action pipeline. By default, `/add-rule` runs mathematical verification (Step 1) before implementation. If `--no-verify` was passed, include `--no-verify` when invoking `/add-rule` to skip verification. Include the concrete details from the issue (problem definition, reduction algorithm, example, etc.) mapped onto each step. -**Plan batching:** The paper writing step (add-model Step 6 / add-rule Step 5) MUST be in a **separate batch** from the implementation steps, so it gets its own subagent with fresh context. It depends on the implementation being complete (needs exports). Example batch structure for a `[Model]` plan: +**Plan batching:** The paper writing step (add-model Step 6 / add-rule Step 6) MUST be in a **separate batch** from the implementation steps, so it gets its own subagent with fresh context. It depends on the implementation being complete (needs exports). Example batch structure for a `[Model]` plan: - Batch 1: Steps 1-5.5 (implement model, register, CLI, tests) - Batch 2: Step 6 (write paper entry — depends on batch 1 for exports) @@ -112,8 +112,8 @@ For a `[Model]` issue with an explicit direct ILP claim, use: - Otherwise, ensure the information provided is enough to implement a solver. **Example rules:** -- Implement the user-provided example instance in the canonical `example_db` path for the issue (`src/example_db/model_builders.rs` or `src/example_db/rule_builders.rs`, as appropriate). -- Run the relevant export and fixture regeneration steps; verify the generated example data against the user-provided information. +- Implement the user-provided example in `src/example_db/model_builders.rs` for a model, or in the rule-local `canonical_rule_example_specs()` for a rule. +- Run the relevant exports and verify the generated example data against the user-provided information. - Present in `docs/paper/reductions.typ` in tutorial style with clear intuition (see KColoring->QUBO section for reference). ### 6. Create PR (or Resume Existing) diff --git a/.claude/skills/review-paper/SKILL.md b/.claude/skills/review-paper/SKILL.md index d1c98cdf6..e5a8e2378 100644 --- a/.claude/skills/review-paper/SKILL.md +++ b/.claude/skills/review-paper/SKILL.md @@ -46,7 +46,7 @@ For each of the 10 entries, read the full entry text and evaluate against the ch | M3. Self-contained notation | Every symbol in `def` is defined before first use | | M4. Background text | Body contains at least 2 sentences of background/motivation | | M5. Example present | Body contains `*Example.*` or `Example.` | -| M6. Example from fixture | Example data matches `src/example_db/fixtures/examples.json` (not invented) — check by loading the JSON and comparing | +| M6. Example from fixture | Example data matches `docs/paper/data/examples.json` (not invented) — check by loading the JSON and comparing | | M7. Figure present | Body contains `#figure(` | | M8. Pred commands | Body contains `pred-commands(` or `pred create` | | M9. Algorithm citation | Complexity claims have `@citation` or a footnote explaining absence | @@ -72,7 +72,7 @@ For each of the 10 entries, read the full entry text and evaluate against the ch | M3. Proof length | Proof is at least 3 sentences (not just "trivial" or a one-liner) | | M4. Overhead documented | Overhead is auto-generated from JSON (verify edge exists in `reduction_graph.json`) | | M5. Example present | `example: true` and example renders correctly | -| M6. Example from fixture | Example data matches `src/example_db/fixtures/examples.json` | +| M6. Example from fixture | Example data matches `docs/paper/data/examples.json` | | M7. Pred commands | Example section contains `pred-commands(` with create/reduce/evaluate pipeline | | M8. Both directions | If the reverse rule also exists in the graph, check it has its own entry | diff --git a/.claude/skills/review-structural/SKILL.md b/.claude/skills/review-structural/SKILL.md index 5c30e15b6..cdf144284 100644 --- a/.claude/skills/review-structural/SKILL.md +++ b/.claude/skills/review-structural/SKILL.md @@ -81,16 +81,16 @@ Only run if review type includes "rule". Given: source `S`, target `T`, rule fil | 6 | Test file exists | `Glob("src/unit_tests/rules/{R}.rs")` | | 7 | Closed-loop test present | `Grep("fn test_.*closed_loop\|fn test_.*to_.*basic", test_file)` | | 8 | Registered in `rules/mod.rs` | `Grep("mod {R}", "src/rules/mod.rs")` | -| 9 | Canonical rule example registered | `Grep("{S}|{T}|{R}", "src/example_db/rule_builders.rs")` | +| 9 | Canonical rule example registered | `Grep("canonical_rule_example_specs", rule file)` and verify it is included by `src/rules/mod.rs` | | 10 | Example-db lookup tests exist | `Grep("find_rule_example|build_rule_db", "src/unit_tests/example_db.rs")` | | 11 | Paper `reduction-rule` entry | `Grep('reduction-rule.*"{S}".*"{T}"', "docs/paper/reductions.typ")` | +| 12 | Extraction contract | Direct decoders call `validate_target_solution()`, enforce rule-specific structure, and test malformed cases; the helper does not establish feasibility or optimality. Composed extractors may delegate. | ## Step 2b: Blacklisted File Check Scan the PR's changed files for auto-generated files that must never be committed: - `docs/src/reductions/reduction_graph.json` - `docs/src/reductions/problem_schemas.json` -- `src/example_db/fixtures/examples.json` (legacy path, deleted on main) - `docs/paper/data/examples.json` (current output path, gitignored) If any of these files appear in the diff, report **FAIL — blacklisted auto-generated file committed**. These files are rebuilt by CI/`make doc`/`make paper` and must not be in PRs. @@ -113,7 +113,7 @@ Report pass/fail. If tests fail, identify which tests. **Do NOT fix anything** 4. **Weight handling** — Are weights managed via inherent methods, not traits? ### For Rules: -1. **`extract_solution` correctness** — Does it correctly invert the reduction? Does the returned solution have the right length (source dimensions)? +1. **`extract_solution` correctness** — Does it implement the mathematical inverse? Is every branch either a defined mathematical case or an `ExtractionError`, with no defaulting, truncation, clamping, panic, or recovery? 2. **Overhead accuracy** — Does `overhead = { field = "expr" }` reflect the actual size relationship? 3. **Example quality** — Is it tutorial-style? Does the JSON export include both source and target data? 4. **Paper quality** — Is the reduction-rule statement precise? Is the proof sketch sound? diff --git a/.claude/skills/write-model-in-paper/SKILL.md b/.claude/skills/write-model-in-paper/SKILL.md index 9ded09507..e3c95d4dd 100644 --- a/.claude/skills/write-model-in-paper/SKILL.md +++ b/.claude/skills/write-model-in-paper/SKILL.md @@ -126,16 +126,16 @@ achieves $O^*(2^n)$ @bjorklund2009. ### 3c. Example with Visualization -A concrete small instance that illustrates the problem. **The example must use data from the checked-in canonical fixture DB**, not an independently invented instance. +A concrete small instance that illustrates the problem. **Use the generated canonical example data**, not an independently invented instance. #### Sourcing example data -1. If you changed example builders/specs, run `make regenerate-fixtures` to refresh `src/example_db/fixtures/examples.json`. -2. Find the problem's entry in `src/example_db/fixtures/examples.json` under `models` — it contains the canonical `instance`, `samples`, and `optimal` fields. +1. If you changed example builders/specs, run `cargo run --features "example-db" --example export_examples`. +2. Find the problem's entry in `docs/paper/data/examples.json` under `models` — it contains the canonical `instance`, `samples`, and `optimal` fields. 3. Use the values from `instance` in the paper example (translating 0-indexed code values to 1-indexed math notation where conventional, e.g., vertices {0,...,n-1} → {1,...,n}). 4. Use `optimal` configurations to show the solution. -**Do not invent a different instance.** If the canonical example is too large or not pedagogically ideal, fix it in `canonical_model_example_specs()` first, re-run `make regenerate-fixtures`, then write the paper entry from the updated JSON. +**Do not invent a different instance.** If the canonical example is unsuitable, fix it in `canonical_model_example_specs()`, re-run `export_examples`, then use the updated JSON. #### Requirements @@ -206,7 +206,7 @@ make paper - [ ] **Notation self-contained**: every symbol in `def` is defined before first use - [ ] **Background present**: historical context, applications, or structural properties - [ ] **Algorithms cited**: every complexity claim has `@citation` or footnote warning -- [ ] **Example from JSON**: instance data matches `src/example_db/fixtures/examples.json` canonical example (not independently invented) +- [ ] **Example from JSON**: instance data matches the canonical entry in `docs/paper/data/examples.json` - [ ] **Evaluation shown**: objective/verifier computed on the example solution - [ ] **Diagram included**: figure with caption and label for graph/matrix/set visualization - [ ] **Paper compiles**: `make paper` succeeds without errors diff --git a/.claude/skills/write-rule-in-paper/SKILL.md b/.claude/skills/write-rule-in-paper/SKILL.md index 0d9755b1b..b1bda1d9c 100644 --- a/.claude/skills/write-rule-in-paper/SKILL.md +++ b/.claude/skills/write-rule-in-paper/SKILL.md @@ -7,7 +7,7 @@ description: Use when writing or improving a reduction-rule entry in the Typst p Full authoring guide for writing a `reduction-rule` entry in `docs/paper/reductions.typ`. Covers Typst mechanics, writing quality, and verification. -> **Note:** This content is also inlined in `add-rule` Step 5 (condensed form). This standalone version has more detail and is useful for improving existing entries. +> **Note:** This content is also inlined in `add-rule` Step 6 (condensed form). This standalone version has more detail and is useful for improving existing entries. ## Reference Example @@ -17,8 +17,8 @@ Full authoring guide for writing a `reduction-rule` entry in `docs/paper/reducti Before using this skill, ensure: - The reduction is implemented and tested (`src/rules/_.rs`) -- A canonical example exists in `src/example_db/rule_builders.rs` -- If the canonical example changed, fixtures are regenerated (`make regenerate-fixtures`) +- A rule-local `canonical_rule_example_specs()` exists and is included by `src/rules/mod.rs` +- If the canonical example changed, regenerate the paper data with `cargo run --features "example-db" --example export_examples` - The reduction graph and schemas are up to date (`cargo run --example export_graph && cargo run --example export_schemas`) ## Source Material @@ -38,7 +38,7 @@ Do NOT invent proofs — always cross-check against the issue and derivation sou ``` Where: -- `load-example(source, target, ...)` looks up the canonical rule entry from `src/example_db/fixtures/examples.json` +- `load-example(source, target, ...)` looks up the canonical rule entry from `docs/paper/data/examples.json` - The returned record contains `source`, `target`, and `solutions` - Access fields: `src_tgt.source.instance`, `src_tgt.target.instance`, `src_tgt_sol.source_config`, `src_tgt_sol.target_config` diff --git a/Makefile b/Makefile index a854eba53..5df8224a0 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ help: @echo " doc - Build mdBook documentation" @echo " diagrams - Generate SVG diagrams from Typst (light + dark)" @echo " mdbook - Build and serve mdBook (with live reload)" - @echo " paper - Build Typst paper from checked-in fixtures (requires typst)" + @echo " paper - Generate example data and build the Typst paper (requires typst)" @echo " coverage - Generate coverage report (requires cargo-llvm-cov)" @echo " clean - Clean build artifacts" @echo " check - Quick check (fmt + clippy + test)" diff --git a/docs/agent-profiles/SKILLS.md b/docs/agent-profiles/SKILLS.md index b7c7a6e3a..df3ffde16 100644 --- a/docs/agent-profiles/SKILLS.md +++ b/docs/agent-profiles/SKILLS.md @@ -1,20 +1,20 @@ # Skills -Example generation now goes through the example catalog and checked-in fixture DB. +Example generation goes through the example catalog and generated paper data. When a workflow needs a paper/example instance, prefer the catalog path over ad hoc `examples/reduction_*.rs` binaries: -- use `src/example_db/fixtures/examples.json` directly for paper/example data -- use `make regenerate-fixtures` when canonical examples change +- use `docs/paper/data/examples.json` directly for paper/example data +- run `cargo run --features "example-db" --example export_examples` when canonical examples change - use `pred create --example ` to materialize a canonical model example as normal problem JSON - use `pred create --example --to ` to materialize a canonical rule example as normal problem JSON - when adding new example coverage, register a catalog entry instead of creating a new standalone reduction example file Post-refactor extension points: -- new model load/serialize/brute-force dispatch comes from `declare_variants!` in the model file, with explicit `opt` or `sat` markers and an optional `default` +- new model load/serialize/brute-force dispatch comes from `declare_variants!` in the model file, with an optional `default` - alias resolution lives in `problemreductions-cli/src/problem_name.rs` - `pred create` UX lives in `problemreductions-cli/src/commands/create.rs` -- canonical examples live in `src/example_db/model_builders.rs` and `src/example_db/rule_builders.rs` +- model examples live in `src/example_db/model_builders.rs`; rule examples live beside their rules and are collected by `src/rules/mod.rs` - [issue-to-pr] — Convert a GitHub issue into a PR with an implementation plan - [add-model] — Add a new problem model to the codebase diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 88cd07556..bdf289f4c 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -65,7 +65,7 @@ #show: thmrules.with(qed-symbol: $square$) // === Example JSON helpers === -// Load canonical example database directly from the checked-in fixture file. +// Load the generated canonical example database. #let example-db = json("data/examples.json") // Pre-index rules by (source, target) and models by name so lookups are O(bucket) diff --git a/docs/src/design.md b/docs/src/design.md index 1fec44b46..20f351018 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -162,12 +162,46 @@ impl ReductionResult for ReductionISToVC { type Target = MinimumVertexCover; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution(&self, target_sol: &[usize]) -> Vec { - target_sol.iter().map(|&x| 1 - x).collect() // complement + fn extract_solution( + &self, + target_sol: &[usize], + ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_sol)?; + Ok(target_sol.iter().map(|&x| 1 - x).collect()) } } ``` +### Solution extraction contract + +`ReductionResult::extract_solution` accepts one complete target configuration +and returns the source configuration defined by the reduction. Extraction is a +fallible boundary, not a recovery mechanism: + +1. In every direct extractor, call `validate_target_solution()` once before + indexing or decoding. Composed extractors delegate this check. +2. Validate any structure required by the inverse mapping, such as exactly-one + blocks, permutations, paths, flows, or schedules. +3. Apply the reduction's mathematical inverse once and return a source + configuration with the required length and domains. +4. Return `ExtractionError` when a precondition is not satisfied. + +Do not truncate or pad input, substitute zero for missing data, select the +first of several invalid candidates, retry with another mapping, or panic on +caller-provided configuration data. Empty and singleton instances should flow +through the same mathematical mapping unless the reduction itself has a +genuine mathematical case distinction. + +Zero and sentinel values remain valid when the source model explicitly gives +them meaning. For example, `MaximumCommonEdgeSubgraph` includes an "unmapped" +sentinel in its source dimensions. Missing target data must never be +interpreted as that sentinel. + +Each conditional in an extractor should therefore either reject a named +invariant violation or implement a case in the reduction's mathematics. A +normal extractor has one validation phase followed by one decoding phase; it +does not accumulate compatibility or fallback branches. + The `#[reduction]` attribute on the `ReduceTo` impl registers the reduction in the global registry (via `inventory`): ```rust,ignore diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 5a48681e7..9c504229a 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -9263,6 +9263,67 @@ fn test_extract_roundtrip_mis_to_qubo() { std::fs::remove_file(&bundle_file).ok(); } +#[test] +fn test_extract_rejects_structurally_invalid_one_hot_config() { + let problem_file = std::env::temp_dir().join("pred_test_extract_tsp_in.json"); + let bundle_file = std::env::temp_dir().join("pred_test_extract_tsp_bundle.json"); + + let create_out = pred() + .args([ + "-o", + problem_file.to_str().unwrap(), + "create", + "TSP", + "--graph", + "0-1,1-2,0-2", + "--edge-weights", + "1,1,1", + ]) + .output() + .unwrap(); + assert!( + create_out.status.success(), + "create stderr: {}", + String::from_utf8_lossy(&create_out.stderr) + ); + + let reduce_out = pred() + .args([ + "-o", + bundle_file.to_str().unwrap(), + "reduce", + problem_file.to_str().unwrap(), + "--to", + "QUBO", + ]) + .output() + .unwrap(); + assert!( + reduce_out.status.success(), + "reduce stderr: {}", + String::from_utf8_lossy(&reduce_out.stderr) + ); + + let extract_out = pred() + .args([ + "extract", + bundle_file.to_str().unwrap(), + "--config", + "0,0,0,0,0,0,0,0,0", + ]) + .output() + .unwrap(); + assert!(!extract_out.status.success()); + let stderr = String::from_utf8(extract_out.stderr).unwrap(); + assert!( + stderr.contains("assignment slot 0 has no selected item"), + "unexpected stderr: {stderr}" + ); + + std::fs::remove_file(&problem_file).ok(); + std::fs::remove_file(&bundle_file).ok(); +} + #[test] fn test_extract_rejects_plain_problem_file() { let problem_file = std::env::temp_dir().join("pred_test_extract_plain.json"); diff --git a/src/models/decision.rs b/src/models/decision.rs index 9f8fddc9e..2d559f4e5 100644 --- a/src/models/decision.rs +++ b/src/models/decision.rs @@ -283,6 +283,8 @@ where &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/acyclicpartition_ilp.rs b/src/rules/acyclicpartition_ilp.rs index 7ce8944ba..39e1d4c6d 100644 --- a/src/rules/acyclicpartition_ilp.rs +++ b/src/rules/acyclicpartition_ilp.rs @@ -29,16 +29,9 @@ impl ReductionResult for ReductionAcyclicPartitionToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let n = self.n; - (0..n) - .map(|v| { - (0..n) - .find(|&c| target_solution[v * n + c] == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, self.n, self.n, 0) } } diff --git a/src/rules/balancedcompletebipartitesubgraph_ilp.rs b/src/rules/balancedcompletebipartitesubgraph_ilp.rs index 754a46b45..39cd6d0a1 100644 --- a/src/rules/balancedcompletebipartitesubgraph_ilp.rs +++ b/src/rules/balancedcompletebipartitesubgraph_ilp.rs @@ -28,6 +28,8 @@ impl ReductionResult for ReductionBCBSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_vertices].to_vec()) } } diff --git a/src/rules/bicliquecover_bmf.rs b/src/rules/bicliquecover_bmf.rs index 887b084fe..f27722628 100644 --- a/src/rules/bicliquecover_bmf.rs +++ b/src/rules/bicliquecover_bmf.rs @@ -40,6 +40,8 @@ impl ReductionResult for ReductionBicliqueCoverToBMF { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(config_bmf_to_bc(target_solution, self.m, self.n, self.k)) } } diff --git a/src/rules/biconnectivityaugmentation_ilp.rs b/src/rules/biconnectivityaugmentation_ilp.rs index c46aa4e98..17b2eff47 100644 --- a/src/rules/biconnectivityaugmentation_ilp.rs +++ b/src/rules/biconnectivityaugmentation_ilp.rs @@ -28,6 +28,8 @@ impl ReductionResult for ReductionBiconnAugToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_candidates].to_vec()) } } diff --git a/src/rules/binpacking_ilp.rs b/src/rules/binpacking_ilp.rs index 4ce03e6a6..e95288c26 100644 --- a/src/rules/binpacking_ilp.rs +++ b/src/rules/binpacking_ilp.rs @@ -9,6 +9,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::BinPacking; use crate::reduction; +use crate::rules::ilp_helpers::one_hot_decode_rows; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing BinPacking to ILP. @@ -40,19 +41,9 @@ impl ReductionResult for ReductionBPToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let n = self.n; - let mut assignment = vec![0usize; n]; - for i in 0..n { - for j in 0..n { - if target_solution[i * n + j] == 1 { - assignment[i] = j; - break; - } - } - } - assignment - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + one_hot_decode_rows(target_solution, self.n, self.n, 0) } } diff --git a/src/rules/bmf_bicliquecover.rs b/src/rules/bmf_bicliquecover.rs index cafa92380..44af229b8 100644 --- a/src/rules/bmf_bicliquecover.rs +++ b/src/rules/bmf_bicliquecover.rs @@ -79,6 +79,8 @@ impl ReductionResult for ReductionBMFToBicliqueCover { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(config_bc_to_bmf(target_solution, self.m, self.n, self.k)) } } diff --git a/src/rules/bmf_ilp.rs b/src/rules/bmf_ilp.rs index 452772dae..3a220cee5 100644 --- a/src/rules/bmf_ilp.rs +++ b/src/rules/bmf_ilp.rs @@ -29,6 +29,8 @@ impl ReductionResult for ReductionBMFToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // Extract B (m x k) then C (k x n) — first m*k + k*n variables let total = self.m * self.k + self.k * self.n; diff --git a/src/rules/bottlenecktravelingsalesman_ilp.rs b/src/rules/bottlenecktravelingsalesman_ilp.rs index a099b26f7..6d3f20452 100644 --- a/src/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/rules/bottlenecktravelingsalesman_ilp.rs @@ -10,6 +10,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BottleneckTravelingSalesman; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; +use crate::rules::ilp_helpers::one_hot_decode; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::Graph; @@ -39,31 +40,28 @@ impl ReductionResult for ReductionBTSPToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_vertices; - // Decode tour: for each position p, find vertex v with x_{v,p} = 1 - let mut tour = vec![0usize; n]; - for p in 0..n { - for v in 0..n { - if target_solution[v * n + p] == 1 { - tour[p] = v; - break; - } - } - } + let tour = one_hot_decode(target_solution, n, n, 0)?; // Map tour to edge selection let mut edge_selection = vec![0usize; self.source_edges.len()]; for p in 0..n { let u = tour[p]; let v = tour[(p + 1) % n]; - for (idx, &(a, b)) in self.source_edges.iter().enumerate() { - if (a == u && b == v) || (a == v && b == u) { - edge_selection[idx] = 1; - break; - } - } + let edge = self + .source_edges + .iter() + .position(|&(a, b)| (a == u && b == v) || (a == v && b == u)) + .ok_or_else(|| { + crate::rules::ExtractionError::invalid(format!( + "target tour uses absent source edge ({u}, {v})" + )) + })?; + edge_selection[edge] = 1; } edge_selection diff --git a/src/rules/boundedcomponentspanningforest_ilp.rs b/src/rules/boundedcomponentspanningforest_ilp.rs index 3722a430c..f5b7f7539 100644 --- a/src/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/rules/boundedcomponentspanningforest_ilp.rs @@ -7,6 +7,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BoundedComponentSpanningForest; use crate::reduction; +use crate::rules::ilp_helpers::one_hot_decode_rows; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::{Graph, SimpleGraph}; @@ -30,17 +31,9 @@ impl ReductionResult for ReductionBCSFToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let n = self.n; - let k = self.k; - (0..n) - .map(|v| { - (0..k) - .find(|&c| target_solution[v * k + c] == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + one_hot_decode_rows(target_solution, self.n, self.k, 0) } } diff --git a/src/rules/capacityassignment_ilp.rs b/src/rules/capacityassignment_ilp.rs index bec8a0981..d7e0f716f 100644 --- a/src/rules/capacityassignment_ilp.rs +++ b/src/rules/capacityassignment_ilp.rs @@ -38,16 +38,14 @@ impl ReductionResult for ReductionCAToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let num_capacities = self.num_capacities; - (0..self.num_links) - .map(|l| { - (0..num_capacities) - .find(|&c| target_solution[l * num_capacities + c] == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.num_links, + self.num_capacities, + 0, + ) } } diff --git a/src/rules/circuit_ilp.rs b/src/rules/circuit_ilp.rs index 76f410ad9..c28ddebef 100644 --- a/src/rules/circuit_ilp.rs +++ b/src/rules/circuit_ilp.rs @@ -40,6 +40,8 @@ impl ReductionResult for ReductionCircuitToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ self.source_variables .iter() diff --git a/src/rules/circuit_sat.rs b/src/rules/circuit_sat.rs index 316d3cb67..384ba8770 100644 --- a/src/rules/circuit_sat.rs +++ b/src/rules/circuit_sat.rs @@ -297,13 +297,9 @@ impl ReductionResult for ReductionCircuitSATToSAT { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - target_solution - .iter() - .take(self.source_var_count) - .copied() - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + Ok(target_solution[..self.source_var_count].to_vec()) } } diff --git a/src/rules/circuit_spinglass.rs b/src/rules/circuit_spinglass.rs index 8ffcb6265..ea27658e9 100644 --- a/src/rules/circuit_spinglass.rs +++ b/src/rules/circuit_spinglass.rs @@ -200,17 +200,13 @@ impl ReductionResult for ReductionCircuitToSG { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - self.source_variables - .iter() - .map(|var| { - self.variable_map - .get(var) - .and_then(|&idx| target_solution.get(idx).copied()) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + Ok(self + .source_variables + .iter() + .map(|variable| target_solution[self.variable_map[variable]]) + .collect()) } } diff --git a/src/rules/closeststring_ilp.rs b/src/rules/closeststring_ilp.rs index 79abbc4b0..16b6b4cbd 100644 --- a/src/rules/closeststring_ilp.rs +++ b/src/rules/closeststring_ilp.rs @@ -55,13 +55,7 @@ impl ReductionResult for ReductionClosestStringToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - if target_solution.len() != self.target.num_vars { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} ILP values, got {}", - self.target.num_vars, - target_solution.len() - ))); - } + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; let q = self.alphabet_size; let mut center = Vec::with_capacity(self.string_length); diff --git a/src/rules/closestsubstring_ilp.rs b/src/rules/closestsubstring_ilp.rs index 77dff6561..c3ac611cc 100644 --- a/src/rules/closestsubstring_ilp.rs +++ b/src/rules/closestsubstring_ilp.rs @@ -75,13 +75,7 @@ impl ReductionResult for ReductionClosestSubstringToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - if target_solution.len() != self.target.num_vars { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} ILP values, got {}", - self.target.num_vars, - target_solution.len() - ))); - } + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; let q = self.alphabet_size; let ell = self.substring_length; diff --git a/src/rules/closestvectorproblem_qubo.rs b/src/rules/closestvectorproblem_qubo.rs index b2046d02e..d53e03dde 100644 --- a/src/rules/closestvectorproblem_qubo.rs +++ b/src/rules/closestvectorproblem_qubo.rs @@ -35,6 +35,8 @@ impl ReductionResult for ReductionCVPToQUBO { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ self.encodings .iter() @@ -43,13 +45,7 @@ impl ReductionResult for ReductionCVPToQUBO { .weights .iter() .enumerate() - .map(|(offset, weight)| { - target_solution - .get(encoding.start + offset) - .copied() - .unwrap_or(0) - * weight - }) + .map(|(offset, weight)| target_solution[encoding.start + offset] * weight) .sum() }) .collect() diff --git a/src/rules/clustering_ilp.rs b/src/rules/clustering_ilp.rs index 00e80e4b4..8fee8f663 100644 --- a/src/rules/clustering_ilp.rs +++ b/src/rules/clustering_ilp.rs @@ -18,12 +18,6 @@ pub struct ReductionClusteringToILP { num_clusters: usize, } -impl ReductionClusteringToILP { - fn var_index(&self, element: usize, cluster: usize) -> usize { - element * self.num_clusters + cluster - } -} - impl ReductionResult for ReductionClusteringToILP { type Source = Clustering; type Target = ILP; @@ -36,18 +30,14 @@ impl ReductionResult for ReductionClusteringToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - (0..self.num_elements) - .map(|element| { - (0..self.num_clusters) - .find(|&cluster| { - let idx = self.var_index(element, cluster); - idx < target_solution.len() && target_solution[idx] == 1 - }) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.num_elements, + self.num_clusters, + 0, + ) } } diff --git a/src/rules/coloring_ilp.rs b/src/rules/coloring_ilp.rs index dd9d4b266..a11a2244d 100644 --- a/src/rules/coloring_ilp.rs +++ b/src/rules/coloring_ilp.rs @@ -10,6 +10,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::KColoring; use crate::reduction; +use crate::rules::ilp_helpers::one_hot_decode_rows; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::{Graph, SimpleGraph}; use crate::variant::{KValue, K1, K2, K3, K4, KN}; @@ -28,13 +29,6 @@ pub struct ReductionKColoringToILP { _phantom: std::marker::PhantomData<(K, G)>, } -impl ReductionKColoringToILP { - /// Get the variable index for vertex v with color c. - fn var_index(&self, vertex: usize, color: usize) -> usize { - vertex * self.num_colors + color - } -} - impl ReductionResult for ReductionKColoringToILP where G: Graph + crate::variant::VariantParam, @@ -54,19 +48,9 @@ where &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let k = self.num_colors; - (0..self.num_vertices) - .map(|v| { - (0..k) - .find(|&c| { - let var_idx = self.var_index(v, c); - var_idx < target_solution.len() && target_solution[var_idx] == 1 - }) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + one_hot_decode_rows(target_solution, self.num_vertices, self.num_colors, 0) } } diff --git a/src/rules/coloring_qubo.rs b/src/rules/coloring_qubo.rs index fede8ccb1..f23fa4b58 100644 --- a/src/rules/coloring_qubo.rs +++ b/src/rules/coloring_qubo.rs @@ -11,6 +11,7 @@ use crate::models::algebraic::QUBO; use crate::models::graph::KColoring; use crate::reduction; +use crate::rules::ilp_helpers::one_hot_decode_rows; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::{Graph, SimpleGraph}; use crate::variant::{KValue, K2, K3, KN}; @@ -37,16 +38,9 @@ impl ReductionResult for ReductionKColoringToQUBO { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let k = self.num_colors; - (0..self.num_vertices) - .map(|v| { - (0..k) - .find(|&c| target_solution[v * k + c] == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + one_hot_decode_rows(target_solution, self.num_vertices, self.num_colors, 0) } } diff --git a/src/rules/consecutiveblockminimization_ilp.rs b/src/rules/consecutiveblockminimization_ilp.rs index 519616040..84c72279f 100644 --- a/src/rules/consecutiveblockminimization_ilp.rs +++ b/src/rules/consecutiveblockminimization_ilp.rs @@ -28,10 +28,9 @@ impl ReductionResult for ReductionCBMToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - // Decode the column permutation from x_{c,p} - one_hot_decode(target_solution, self.num_cols, self.num_cols, 0) - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + one_hot_decode(target_solution, self.num_cols, self.num_cols, 0) } } diff --git a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs index aadf9abd0..f2b996dba 100644 --- a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs +++ b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs @@ -29,12 +29,9 @@ impl ReductionResult for ReductionCOMAToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok(one_hot_decode( - target_solution, - self.num_cols, - self.num_cols, - 0, - )) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + one_hot_decode(target_solution, self.num_cols, self.num_cols, 0) } } diff --git a/src/rules/consecutiveonessubmatrix_ilp.rs b/src/rules/consecutiveonessubmatrix_ilp.rs index 03bb93dcf..a15914f95 100644 --- a/src/rules/consecutiveonessubmatrix_ilp.rs +++ b/src/rules/consecutiveonessubmatrix_ilp.rs @@ -26,6 +26,8 @@ impl ReductionResult for ReductionCOSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // Output the selection bits s_c (first num_cols variables) target_solution[..self.num_cols].to_vec() diff --git a/src/rules/consistencyofdatabasefrequencytables_ilp.rs b/src/rules/consistencyofdatabasefrequencytables_ilp.rs index 712e0509a..d6fdac1fa 100644 --- a/src/rules/consistencyofdatabasefrequencytables_ilp.rs +++ b/src/rules/consistencyofdatabasefrequencytables_ilp.rs @@ -94,20 +94,30 @@ impl ReductionResult for ReductionCDFTToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let mut source_solution = Vec::with_capacity(self.source.num_assignment_variables()); for object in 0..self.source.num_objects() { for (attribute, &domain_size) in self.source.attribute_domains().iter().enumerate() { - let value = (0..domain_size) - .find(|&candidate| { - target_solution - .get(self.assignment_var_index(object, attribute, candidate)) - .copied() - .unwrap_or(0) - == 1 - }) - .unwrap_or(0); + let mut selected = (0..domain_size).filter(|&candidate| { + target_solution[self.assignment_var_index(object, attribute, candidate)] + == 1 + }); + let value = match (selected.next(), selected.next()) { + (Some(value), None) => value, + (None, _) => { + return Err(crate::rules::ExtractionError::invalid(format!( + "object {object}, attribute {attribute} has no selected value" + ))) + } + (Some(_), Some(_)) => { + return Err(crate::rules::ExtractionError::invalid(format!( + "object {object}, attribute {attribute} has multiple selected values" + ))) + } + }; source_solution.push(value); } } diff --git a/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs b/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs index 104180d06..ba8a9fb66 100644 --- a/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs +++ b/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs @@ -28,6 +28,8 @@ impl ReductionResult for ReductionDecisionMinimumDominatingSetToMinimumSumMultic &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs b/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs index 38bfdb5ff..a475a5667 100644 --- a/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs +++ b/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs @@ -28,6 +28,8 @@ impl ReductionResult for ReductionDecisionMinimumDominatingSetToMinMaxMulticente &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs b/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs index 99a082038..88148866a 100644 --- a/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs +++ b/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs @@ -182,7 +182,7 @@ impl TheoremConstruction { witness } - fn extract_solution( + fn decode_solution( &self, target_problem: &HamiltonianCircuit, target_solution: &[usize], @@ -267,6 +267,8 @@ impl ReductionResult for ReductionDecisionMinimumVertexCoverToHamiltonianCircuit &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ match &self.construction { ConstructionKind::FixedYes { source_cover } => { @@ -284,7 +286,7 @@ impl ReductionResult for ReductionDecisionMinimumVertexCoverToHamiltonianCircuit )) } ConstructionKind::Theorem(construction) => { - construction.extract_solution(&self.target, target_solution)? + construction.decode_solution(&self.target, target_solution)? } } }) diff --git a/src/rules/directedhamiltonianpath_ilp.rs b/src/rules/directedhamiltonianpath_ilp.rs index 52edd18a6..e64b036e9 100644 --- a/src/rules/directedhamiltonianpath_ilp.rs +++ b/src/rules/directedhamiltonianpath_ilp.rs @@ -36,10 +36,12 @@ impl ReductionResult for ReductionDirectedHamiltonianPathToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_vertices; // Decode one-hot assignment: permutation[k] = v where x_{v,k} = 1 - let perm = one_hot_decode(target_solution, n, n, 0); + let perm = one_hot_decode(target_solution, n, n, 0)?; permutation_to_lehmer(&perm) }) } diff --git a/src/rules/directedtwocommodityintegralflow_ilp.rs b/src/rules/directedtwocommodityintegralflow_ilp.rs index 013e3f684..890d239f7 100644 --- a/src/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/rules/directedtwocommodityintegralflow_ilp.rs @@ -41,6 +41,8 @@ impl ReductionResult for ReductionD2CIFToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..2 * self.num_arcs].to_vec()) } } diff --git a/src/rules/disjointconnectingpaths_ilp.rs b/src/rules/disjointconnectingpaths_ilp.rs index fb4fb415c..c941816ab 100644 --- a/src/rules/disjointconnectingpaths_ilp.rs +++ b/src/rules/disjointconnectingpaths_ilp.rs @@ -38,6 +38,8 @@ impl ReductionResult for ReductionDCPToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // Mark an edge selected iff some orientation carries flow for some commodity. let m = self.edges.len(); diff --git a/src/rules/eulerianpath_ilp.rs b/src/rules/eulerianpath_ilp.rs index 468bb6bdc..70502b17f 100644 --- a/src/rules/eulerianpath_ilp.rs +++ b/src/rules/eulerianpath_ilp.rs @@ -74,6 +74,8 @@ impl ReductionResult for ReductionEulerianPathToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let m = self.num_arcs; if m == 0 { @@ -81,9 +83,7 @@ impl ReductionResult for ReductionEulerianPathToILP { } // Find the unique active start arc. - let mut current = match (0..m) - .find(|&a| target_solution.get(self.s_idx(a)).copied().unwrap_or(0) == 1) - { + let mut current = match (0..m).find(|&a| target_solution[self.s_idx(a)] == 1) { Some(a) => a, None => { return Err(crate::rules::ExtractionError::invalid( @@ -103,9 +103,7 @@ impl ReductionResult for ReductionEulerianPathToILP { .pairs .iter() .enumerate() - .find(|&(k, &(a, _))| { - a == current && target_solution.get(k).copied().unwrap_or(0) == 1 - }) + .find(|&(k, &(a, _))| a == current && target_solution[k] == 1) .map(|(_, &(_, b))| b); match next { diff --git a/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs b/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs index a94de0a8b..a8aacac1e 100644 --- a/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs +++ b/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs @@ -22,6 +22,8 @@ impl ReductionResult for ReductionX3CToAlgebraicEquationsOverGF2 { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs b/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs index 22aa8da46..882c3b958 100644 --- a/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs +++ b/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs @@ -62,19 +62,13 @@ impl ReductionResult for ReductionX3CToBoundedDiameterSpanningTree { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let m = self.source_num_subsets; let root_to_set_offset = 2; (0..m) - .map(|i| { - usize::from( - target_solution - .get(root_to_set_offset + i) - .copied() - .unwrap_or(0) - == 1, - ) - }) + .map(|i| usize::from(target_solution[root_to_set_offset + i] == 1)) .collect() }) } diff --git a/src/rules/exactcoverby3sets_ilp.rs b/src/rules/exactcoverby3sets_ilp.rs index 8a9f2e4c6..37455375f 100644 --- a/src/rules/exactcoverby3sets_ilp.rs +++ b/src/rules/exactcoverby3sets_ilp.rs @@ -25,6 +25,8 @@ impl ReductionResult for ReductionX3CToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/exactcoverby3sets_maximumsetpacking.rs b/src/rules/exactcoverby3sets_maximumsetpacking.rs index 8155b236e..b5f5d6815 100644 --- a/src/rules/exactcoverby3sets_maximumsetpacking.rs +++ b/src/rules/exactcoverby3sets_maximumsetpacking.rs @@ -33,6 +33,8 @@ impl ReductionResult for ReductionXC3SToMaximumSetPacking { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/exactcoverby3sets_minimumaxiomset.rs b/src/rules/exactcoverby3sets_minimumaxiomset.rs index d1035a9a7..a6df6546c 100644 --- a/src/rules/exactcoverby3sets_minimumaxiomset.rs +++ b/src/rules/exactcoverby3sets_minimumaxiomset.rs @@ -33,10 +33,12 @@ impl ReductionResult for ReductionXC3SToMinimumAxiomSet { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let set_offset = self.source_universe_size; (0..self.source_num_subsets) - .map(|j| usize::from(target_solution.get(set_offset + j).copied().unwrap_or(0) > 0)) + .map(|j| usize::from(target_solution[set_offset + j] > 0)) .collect() }) } diff --git a/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs b/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs index e16724e38..7e69ccf21 100644 --- a/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs +++ b/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs @@ -28,6 +28,8 @@ impl ReductionResult for ReductionXC3SToMinimumFaultDetectionTestSet { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/exactcoverby3sets_staffscheduling.rs b/src/rules/exactcoverby3sets_staffscheduling.rs index 70585f0bd..da5fbb997 100644 --- a/src/rules/exactcoverby3sets_staffscheduling.rs +++ b/src/rules/exactcoverby3sets_staffscheduling.rs @@ -37,6 +37,8 @@ impl ReductionResult for ReductionXC3SToStaffScheduling { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ target_solution .iter() diff --git a/src/rules/exactcoverby3sets_subsetproduct.rs b/src/rules/exactcoverby3sets_subsetproduct.rs index 0662a9295..7df3637a4 100644 --- a/src/rules/exactcoverby3sets_subsetproduct.rs +++ b/src/rules/exactcoverby3sets_subsetproduct.rs @@ -30,6 +30,8 @@ impl ReductionResult for ReductionX3CToSubsetProduct { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/expectedretrievalcost_ilp.rs b/src/rules/expectedretrievalcost_ilp.rs index 5e6d88acf..7fdb15d95 100644 --- a/src/rules/expectedretrievalcost_ilp.rs +++ b/src/rules/expectedretrievalcost_ilp.rs @@ -17,6 +17,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ExpectedRetrievalCost; use crate::reduction; +use crate::rules::ilp_helpers::one_hot_decode_rows; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Compute the latency distance between sectors on a circular device. @@ -69,19 +70,9 @@ impl ReductionResult for ReductionERCToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let num_sectors = self.num_sectors; - (0..self.num_records) - .map(|r| { - (0..num_sectors) - .find(|&s| { - let idx = r * num_sectors + s; - idx < target_solution.len() && target_solution[idx] == 1 - }) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + one_hot_decode_rows(target_solution, self.num_records, self.num_sectors, 0) } } diff --git a/src/rules/factoring_circuit.rs b/src/rules/factoring_circuit.rs index af5ad802e..4f7d4541e 100644 --- a/src/rules/factoring_circuit.rs +++ b/src/rules/factoring_circuit.rs @@ -46,6 +46,8 @@ impl ReductionResult for ReductionFactoringToCircuit { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let var_names = self.target.variable_names(); @@ -53,27 +55,20 @@ impl ReductionResult for ReductionFactoringToCircuit { let var_map: std::collections::HashMap<&str, usize> = var_names .iter() .enumerate() - .map(|(i, name)| (name.as_str(), target_solution.get(i).copied().unwrap_or(0))) - .collect(); - - // Extract p bits - let p_bits: Vec = self - .p_vars - .iter() - .map(|name| *var_map.get(name.as_str()).unwrap_or(&0)) + .map(|(i, name)| (name.as_str(), target_solution[i])) .collect(); - // Extract q bits - let q_bits: Vec = self - .q_vars + self.p_vars .iter() - .map(|name| *var_map.get(name.as_str()).unwrap_or(&0)) - .collect(); - - // Concatenate p and q bits - let mut result = p_bits; - result.extend(q_bits); - result + .chain(&self.q_vars) + .map(|name| { + var_map.get(name.as_str()).copied().ok_or_else(|| { + crate::rules::ExtractionError::invalid(format!( + "target circuit does not contain factor variable {name}" + )) + }) + }) + .collect::>>()? }) } } diff --git a/src/rules/factoring_ilp.rs b/src/rules/factoring_ilp.rs index 51d3ea332..4f52fa2e3 100644 --- a/src/rules/factoring_ilp.rs +++ b/src/rules/factoring_ilp.rs @@ -79,15 +79,17 @@ impl ReductionResult for ReductionFactoringToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // Extract p bits (first factor) let p_bits: Vec = (0..self.m) - .map(|i| target_solution.get(self.p_var(i)).copied().unwrap_or(0)) + .map(|i| target_solution[self.p_var(i)]) .collect(); // Extract q bits (second factor) let q_bits: Vec = (0..self.n) - .map(|j| target_solution.get(self.q_var(j)).copied().unwrap_or(0)) + .map(|j| target_solution[self.q_var(j)]) .collect(); // Concatenate p and q bits diff --git a/src/rules/feasibleregisterassignment_ilp.rs b/src/rules/feasibleregisterassignment_ilp.rs index ad0028b63..b12ab41f8 100644 --- a/src/rules/feasibleregisterassignment_ilp.rs +++ b/src/rules/feasibleregisterassignment_ilp.rs @@ -33,6 +33,8 @@ impl ReductionResult for ReductionFeasibleRegisterAssignmentToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_vertices].to_vec()) } } diff --git a/src/rules/flowshopscheduling_ilp.rs b/src/rules/flowshopscheduling_ilp.rs index 14c0de42f..712edbd62 100644 --- a/src/rules/flowshopscheduling_ilp.rs +++ b/src/rules/flowshopscheduling_ilp.rs @@ -57,6 +57,8 @@ impl ReductionResult for ReductionFSSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_jobs; let m = self.num_machines; @@ -64,7 +66,7 @@ impl ReductionResult for ReductionFSSToILP { let mut jobs: Vec = (0..n).collect(); jobs.sort_by_key(|&j| { let idx = c_offset + j * m + (m - 1); - (target_solution.get(idx).copied().unwrap_or(0), j) + (target_solution[idx], j) }); let perm = permutation_to_lehmer(&jobs); Self::encode_schedule_as_lehmer(&jobs) diff --git a/src/rules/graphpartitioning_ilp.rs b/src/rules/graphpartitioning_ilp.rs index 5f04aa3c7..ffb6a1edf 100644 --- a/src/rules/graphpartitioning_ilp.rs +++ b/src/rules/graphpartitioning_ilp.rs @@ -34,6 +34,8 @@ impl ReductionResult for ReductionGraphPartitioningToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_vertices].to_vec()) } } diff --git a/src/rules/graphpartitioning_maxcut.rs b/src/rules/graphpartitioning_maxcut.rs index 5ab10a2bc..2e7985fd3 100644 --- a/src/rules/graphpartitioning_maxcut.rs +++ b/src/rules/graphpartitioning_maxcut.rs @@ -26,6 +26,8 @@ impl ReductionResult for ReductionGPToMaxCut { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/graphpartitioning_qubo.rs b/src/rules/graphpartitioning_qubo.rs index ca592d8c9..b9f86d3a9 100644 --- a/src/rules/graphpartitioning_qubo.rs +++ b/src/rules/graphpartitioning_qubo.rs @@ -28,6 +28,8 @@ impl ReductionResult for ReductionGraphPartitioningToQUBO { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs b/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs index 9b7b3bdc4..5b0dd9230 100644 --- a/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs +++ b/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs @@ -48,6 +48,8 @@ impl ReductionResult for ReductionHamiltonianCircuitToBiconnectivityAugmentation &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_vertices; if n < 3 { @@ -59,7 +61,7 @@ impl ReductionResult for ReductionHamiltonianCircuitToBiconnectivityAugmentation // Collect selected edges (those with config value 1) let mut adj: Vec> = vec![vec![]; n]; for (i, &(u, v)) in self.potential_edges.iter().enumerate() { - if i < target_solution.len() && target_solution[i] == 1 { + if target_solution[i] == 1 { adj[u].push(v); adj[v].push(u); } diff --git a/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs b/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs index 34c6f6e5a..19fd534b1 100644 --- a/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs +++ b/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs @@ -27,6 +27,8 @@ impl ReductionResult for ReductionHamiltonianCircuitToBottleneckTravelingSalesma &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + crate::rules::graph_helpers::edges_to_cycle_order(self.target.graph(), target_solution) } } diff --git a/src/rules/hamiltoniancircuit_hamiltonianpath.rs b/src/rules/hamiltoniancircuit_hamiltonianpath.rs index 1a7ad073d..aa83c15d8 100644 --- a/src/rules/hamiltoniancircuit_hamiltonianpath.rs +++ b/src/rules/hamiltoniancircuit_hamiltonianpath.rs @@ -40,20 +40,14 @@ impl ReductionResult for ReductionHamiltonianCircuitToHamiltonianPath { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_original_vertices; if n == 0 { return Ok(vec![]); } - if target_solution.len() != n + 3 { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} path vertices, got {}", - n + 3, - target_solution.len() - ))); - } - let v_prime = n; // index of duplicated vertex v' let s = n + 1; // pendant attached to v=0 let t = n + 2; // pendant attached to v' diff --git a/src/rules/hamiltoniancircuit_longestcircuit.rs b/src/rules/hamiltoniancircuit_longestcircuit.rs index 3fc0d3d4a..701292bd9 100644 --- a/src/rules/hamiltoniancircuit_longestcircuit.rs +++ b/src/rules/hamiltoniancircuit_longestcircuit.rs @@ -27,6 +27,8 @@ impl ReductionResult for ReductionHamiltonianCircuitToLongestCircuit { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + crate::rules::graph_helpers::edges_to_cycle_order(self.target.graph(), target_solution) } } diff --git a/src/rules/hamiltoniancircuit_quadraticassignment.rs b/src/rules/hamiltoniancircuit_quadraticassignment.rs index d5c4a5571..d03c564ce 100644 --- a/src/rules/hamiltoniancircuit_quadraticassignment.rs +++ b/src/rules/hamiltoniancircuit_quadraticassignment.rs @@ -30,6 +30,8 @@ impl ReductionResult for ReductionHamiltonianCircuitToQuadraticAssignment { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // QAP config is a permutation γ mapping positions to vertices, // which is directly the Hamiltonian circuit visit order. diff --git a/src/rules/hamiltoniancircuit_ruralpostman.rs b/src/rules/hamiltoniancircuit_ruralpostman.rs index f9b879091..31f8669e7 100644 --- a/src/rules/hamiltoniancircuit_ruralpostman.rs +++ b/src/rules/hamiltoniancircuit_ruralpostman.rs @@ -50,6 +50,8 @@ impl ReductionResult for ReductionHamiltonianCircuitToRuralPostman { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // The target solution is edge multiplicities. // Required edges are indices 0..n (the {v_i^a, v_i^b} edges). @@ -69,8 +71,8 @@ impl ReductionResult for ReductionHamiltonianCircuitToRuralPostman { let fwd_idx = n + 2 * k; // {v_i^b, v_j^a} let bwd_idx = n + 2 * k + 1; // {v_j^b, v_i^a} - let fwd_mult = target_solution.get(fwd_idx).copied().unwrap_or(0); - let bwd_mult = target_solution.get(bwd_idx).copied().unwrap_or(0); + let fwd_mult = target_solution[fwd_idx]; + let bwd_mult = target_solution[bwd_idx]; // In an optimal HC solution, each connectivity edge is used 0 or 1 times. // Each vertex should have exactly one outgoing connectivity edge. diff --git a/src/rules/hamiltoniancircuit_stackercrane.rs b/src/rules/hamiltoniancircuit_stackercrane.rs index 86f5900d6..8408e8d78 100644 --- a/src/rules/hamiltoniancircuit_stackercrane.rs +++ b/src/rules/hamiltoniancircuit_stackercrane.rs @@ -36,6 +36,8 @@ impl ReductionResult for ReductionHamiltonianCircuitToStackerCrane { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // The target config is a permutation of arc indices. // Arc i corresponds to original vertex i (arc from 2i to 2i+1). diff --git a/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs b/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs index e56791739..e4b87c770 100644 --- a/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs +++ b/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs @@ -31,6 +31,8 @@ impl ReductionResult for ReductionHamiltonianCircuitToStrongConnectivityAugmenta &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.n; if n == 0 { diff --git a/src/rules/hamiltoniancircuit_travelingsalesman.rs b/src/rules/hamiltoniancircuit_travelingsalesman.rs index 19ba0211f..d58b5518d 100644 --- a/src/rules/hamiltoniancircuit_travelingsalesman.rs +++ b/src/rules/hamiltoniancircuit_travelingsalesman.rs @@ -27,6 +27,8 @@ impl ReductionResult for ReductionHamiltonianCircuitToTravelingSalesman { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + crate::rules::graph_helpers::edges_to_cycle_order(self.target.graph(), target_solution) } } diff --git a/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs b/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs index 5cc4085ac..0ea5af57b 100644 --- a/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs +++ b/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs @@ -25,6 +25,8 @@ impl ReductionResult for ReductionHamiltonianPathToDegreeConstrainedSpanningTree &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + extract_hamiltonian_order(self.target.graph(), target_solution) } } @@ -57,14 +59,6 @@ fn extract_hamiltonian_order( } let edges = graph.edges(); - if target_solution.len() != edges.len() { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} edge-selection values, got {}", - edges.len(), - target_solution.len() - ))); - } - let mut adjacency = vec![Vec::new(); num_vertices]; for ((u, v), &selected) in edges.iter().copied().zip(target_solution.iter()) { if selected != 1 { diff --git a/src/rules/hamiltonianpath_ilp.rs b/src/rules/hamiltonianpath_ilp.rs index c15336d73..f646ed94b 100644 --- a/src/rules/hamiltonianpath_ilp.rs +++ b/src/rules/hamiltonianpath_ilp.rs @@ -39,12 +39,9 @@ impl ReductionResult for ReductionHamiltonianPathToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok(one_hot_decode( - target_solution, - self.num_vertices, - self.num_vertices, - 0, - )) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + one_hot_decode(target_solution, self.num_vertices, self.num_vertices, 0) } } diff --git a/src/rules/hamiltonianpath_isomorphicspanningtree.rs b/src/rules/hamiltonianpath_isomorphicspanningtree.rs index 5e4687483..939ba38d3 100644 --- a/src/rules/hamiltonianpath_isomorphicspanningtree.rs +++ b/src/rules/hamiltonianpath_isomorphicspanningtree.rs @@ -32,6 +32,8 @@ impl ReductionResult for ReductionHPToIST { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs b/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs index 51d67471f..cd96b1d0c 100644 --- a/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs +++ b/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs @@ -37,6 +37,8 @@ impl ReductionResult for ReductionHPBTVToLP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_vertices; diff --git a/src/rules/highlyconnecteddeletion_ilp.rs b/src/rules/highlyconnecteddeletion_ilp.rs index eaff32c3e..227049a8f 100644 --- a/src/rules/highlyconnecteddeletion_ilp.rs +++ b/src/rules/highlyconnecteddeletion_ilp.rs @@ -64,13 +64,7 @@ impl ReductionResult for ReductionHighlyConnectedDeletionToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - if target_solution.len() != self.clusters.len() { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} cluster-selection values, got {}", - self.clusters.len(), - target_solution.len() - ))); - } + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; let mut cluster_of: Vec> = vec![None; vertex_count(&self.clusters)]; for (c, cluster) in self.clusters.iter().enumerate() { diff --git a/src/rules/ilp_bool_ilp_i32.rs b/src/rules/ilp_bool_ilp_i32.rs index 7df8576c3..172846b64 100644 --- a/src/rules/ilp_bool_ilp_i32.rs +++ b/src/rules/ilp_bool_ilp_i32.rs @@ -28,6 +28,8 @@ impl ReductionResult for ReductionBinaryILPToIntILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/ilp_helpers.rs b/src/rules/ilp_helpers.rs index db5294571..93fe410ac 100644 --- a/src/rules/ilp_helpers.rs +++ b/src/rules/ilp_helpers.rs @@ -140,12 +140,56 @@ pub fn one_hot_decode( num_items: usize, num_slots: usize, var_offset: usize, -) -> Vec { - (0..num_slots) +) -> crate::rules::ExtractionResult> { + let assignment: Vec = (0..num_slots) .map(|p| { - (0..num_items) - .find(|&v| solution[var_offset + v * num_slots + p] == 1) - .unwrap_or(0) + let mut selected = + (0..num_items).filter(|&v| solution[var_offset + v * num_slots + p] == 1); + let item = selected.next().ok_or_else(|| { + crate::rules::ExtractionError::invalid(format!( + "assignment slot {p} has no selected item" + )) + })?; + if selected.next().is_some() { + return Err(crate::rules::ExtractionError::invalid(format!( + "assignment slot {p} has multiple selected items" + ))); + } + Ok(item) + }) + .collect::>()?; + + let mut assigned = vec![false; num_items]; + for &item in &assignment { + if std::mem::replace(&mut assigned[item], true) { + return Err(crate::rules::ExtractionError::invalid(format!( + "item {item} is selected for multiple assignment slots" + ))); + } + } + Ok(assignment) +} + +/// Decode one selected column from each row of a row-major binary matrix. +pub fn one_hot_decode_rows( + solution: &[usize], + num_rows: usize, + num_columns: usize, + var_offset: usize, +) -> crate::rules::ExtractionResult> { + (0..num_rows) + .map(|row| { + let mut selected = (0..num_columns) + .filter(|&column| solution[var_offset + row * num_columns + column] == 1); + match (selected.next(), selected.next()) { + (Some(column), None) => Ok(column), + (None, _) => Err(crate::rules::ExtractionError::invalid(format!( + "assignment row {row} has no selected column" + ))), + (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( + "assignment row {row} has multiple selected columns" + ))), + } }) .collect() } diff --git a/src/rules/ilp_i32_ilp_bool.rs b/src/rules/ilp_i32_ilp_bool.rs index 53d1cfbf9..c2313e637 100644 --- a/src/rules/ilp_i32_ilp_bool.rs +++ b/src/rules/ilp_i32_ilp_bool.rs @@ -251,6 +251,8 @@ impl ReductionResult for ReductionIntILPToBinaryILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ self.encodings .iter() diff --git a/src/rules/ilp_qubo.rs b/src/rules/ilp_qubo.rs index 9e099a241..7549af386 100644 --- a/src/rules/ilp_qubo.rs +++ b/src/rules/ilp_qubo.rs @@ -33,6 +33,8 @@ impl ReductionResult for ReductionILPToQUBO { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_original_vars].to_vec()) } } diff --git a/src/rules/integerknapsack_ilp.rs b/src/rules/integerknapsack_ilp.rs index 6b8afb4a1..c0a4719bb 100644 --- a/src/rules/integerknapsack_ilp.rs +++ b/src/rules/integerknapsack_ilp.rs @@ -26,6 +26,8 @@ impl ReductionResult for ReductionIntegerKnapsackToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/integralflowbundles_ilp.rs b/src/rules/integralflowbundles_ilp.rs index 70d1823b5..904146977 100644 --- a/src/rules/integralflowbundles_ilp.rs +++ b/src/rules/integralflowbundles_ilp.rs @@ -27,6 +27,8 @@ impl ReductionResult for ReductionIFBToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/integralflowhomologousarcs_ilp.rs b/src/rules/integralflowhomologousarcs_ilp.rs index 8d810fb1a..9c36712d7 100644 --- a/src/rules/integralflowhomologousarcs_ilp.rs +++ b/src/rules/integralflowhomologousarcs_ilp.rs @@ -26,6 +26,8 @@ impl ReductionResult for ReductionIFHAToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/integralflowwithmultipliers_ilp.rs b/src/rules/integralflowwithmultipliers_ilp.rs index f52533bb4..56ed71ddf 100644 --- a/src/rules/integralflowwithmultipliers_ilp.rs +++ b/src/rules/integralflowwithmultipliers_ilp.rs @@ -26,6 +26,8 @@ impl ReductionResult for ReductionIFWMToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/isomorphicspanningtree_ilp.rs b/src/rules/isomorphicspanningtree_ilp.rs index c28f3cfd9..306977592 100644 --- a/src/rules/isomorphicspanningtree_ilp.rs +++ b/src/rules/isomorphicspanningtree_ilp.rs @@ -28,16 +28,9 @@ impl ReductionResult for ReductionISTToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let n = self.n; - (0..n) - .map(|u| { - (0..n) - .find(|&v| target_solution[u * n + v] == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, self.n, self.n, 0) } } diff --git a/src/rules/kclique_balancedcompletebipartitesubgraph.rs b/src/rules/kclique_balancedcompletebipartitesubgraph.rs index 6817bf98e..d38e05cfc 100644 --- a/src/rules/kclique_balancedcompletebipartitesubgraph.rs +++ b/src/rules/kclique_balancedcompletebipartitesubgraph.rs @@ -38,6 +38,8 @@ impl ReductionResult for ReductionKCliqueToBCBS { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ (0..self.num_original_vertices) .map(|v| 1 - target_solution[v]) diff --git a/src/rules/kclique_conjunctivebooleanquery.rs b/src/rules/kclique_conjunctivebooleanquery.rs index 273ca0d00..4e1d5c153 100644 --- a/src/rules/kclique_conjunctivebooleanquery.rs +++ b/src/rules/kclique_conjunctivebooleanquery.rs @@ -38,6 +38,8 @@ impl ReductionResult for ReductionKCliqueToCBQ { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(KClique::::config_from_vertices( self.num_vertices, target_solution, diff --git a/src/rules/kclique_ilp.rs b/src/rules/kclique_ilp.rs index 4e15084bf..35f985500 100644 --- a/src/rules/kclique_ilp.rs +++ b/src/rules/kclique_ilp.rs @@ -43,6 +43,8 @@ impl ReductionResult for ReductionKCliqueToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/kclique_subgraphisomorphism.rs b/src/rules/kclique_subgraphisomorphism.rs index 3e8c518f2..e4c4c8147 100644 --- a/src/rules/kclique_subgraphisomorphism.rs +++ b/src/rules/kclique_subgraphisomorphism.rs @@ -38,6 +38,8 @@ impl ReductionResult for ReductionKCliqueToSubIso { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ KClique::::config_from_vertices(self.num_source_vertices, target_solution) }) diff --git a/src/rules/kcoloring_bicliquecover.rs b/src/rules/kcoloring_bicliquecover.rs index cdaeb6507..b65f9e751 100644 --- a/src/rules/kcoloring_bicliquecover.rs +++ b/src/rules/kcoloring_bicliquecover.rs @@ -68,13 +68,12 @@ impl ReductionResult for ReductionKColoringToBicliqueCover { /// cover yields at most `q` such distinct bicliques, so the result is a /// proper `q`-coloring of the source. /// - /// If the witness is invalid (e.g. some diagonal edge is uncovered), - /// the extracted entry for `v` falls back to color `0`. Validation - /// downstream is the responsibility of `source.is_valid_solution`. fn extract_solution( &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_vertices; let k = self.target.k(); @@ -82,40 +81,36 @@ impl ReductionResult for ReductionKColoringToBicliqueCover { // For each source vertex v, find the first biclique r that contains // both a_v (unified index v) and b_v (unified index left_size + v). - let mut diagonal_biclique = vec![None; n]; - for (v, slot) in diagonal_biclique.iter_mut().enumerate() { + let mut diagonal_biclique = Vec::with_capacity(n); + for v in 0..n { let a_v = v; let b_v = left_size + v; - for r in 0..k { - let a_idx = a_v * k + r; - let b_idx = b_v * k + r; - if target_solution.get(a_idx).copied().unwrap_or(0) == 1 - && target_solution.get(b_idx).copied().unwrap_or(0) == 1 - { - *slot = Some(r); - break; - } - } + let biclique = (0..k) + .find(|&r| { + target_solution[a_v * k + r] == 1 && target_solution[b_v * k + r] == 1 + }) + .ok_or_else(|| { + crate::rules::ExtractionError::invalid(format!( + "target cover leaves diagonal gadget edge {v} uncovered" + )) + })?; + diagonal_biclique.push(biclique); } // Compact distinct biclique indices into colors 0..q-1 in first-seen order. let mut color_of_biclique: std::collections::HashMap = std::collections::HashMap::new(); - let mut coloring = vec![0usize; n]; - for (v, slot) in diagonal_biclique.iter().enumerate() { - if let Some(r) = *slot { - let next_color = color_of_biclique.len(); - let color = *color_of_biclique.entry(r).or_insert(next_color); - // Clamp into [0, q-1]: if the witness exceeds q distinct - // diagonal bicliques (which a valid cover never does) keep - // the entry in range so the downstream validator can - // simply reject it as an improper coloring. - coloring[v] = if self.num_colors == 0 { - 0 - } else { - color.min(self.num_colors - 1) - }; + let mut coloring = Vec::with_capacity(n); + for biclique in diagonal_biclique { + let next_color = color_of_biclique.len(); + let color = *color_of_biclique.entry(biclique).or_insert(next_color); + if color >= self.num_colors { + return Err(crate::rules::ExtractionError::invalid(format!( + "target cover uses more than {} diagonal bicliques", + self.num_colors + ))); } + coloring.push(color); } coloring }) diff --git a/src/rules/kcoloring_clustering.rs b/src/rules/kcoloring_clustering.rs index 79b77e7b2..23af4cb85 100644 --- a/src/rules/kcoloring_clustering.rs +++ b/src/rules/kcoloring_clustering.rs @@ -32,7 +32,9 @@ impl ReductionResult for ReductionKColoringToClustering { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok(target_solution[..self.source_num_vertices.min(target_solution.len())].to_vec()) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + Ok(target_solution[..self.source_num_vertices].to_vec()) } } diff --git a/src/rules/kcoloring_partitionintocliques.rs b/src/rules/kcoloring_partitionintocliques.rs index 3fc634caa..0858828bf 100644 --- a/src/rules/kcoloring_partitionintocliques.rs +++ b/src/rules/kcoloring_partitionintocliques.rs @@ -29,6 +29,8 @@ impl ReductionResult for ReductionKColoringToPartitionIntoCliques { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/kcoloring_twodimensionalconsecutivesets.rs b/src/rules/kcoloring_twodimensionalconsecutivesets.rs index 2a7208af6..88fd78f20 100644 --- a/src/rules/kcoloring_twodimensionalconsecutivesets.rs +++ b/src/rules/kcoloring_twodimensionalconsecutivesets.rs @@ -43,6 +43,8 @@ impl ReductionResult for ReductionKColoringToTDCS { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // The target solution is config[symbol] = group_index. // Vertex symbols are indices 0..num_vertices. diff --git a/src/rules/knapsack_ilp.rs b/src/rules/knapsack_ilp.rs index ffa4c2473..11b6d3f16 100644 --- a/src/rules/knapsack_ilp.rs +++ b/src/rules/knapsack_ilp.rs @@ -28,6 +28,8 @@ impl ReductionResult for ReductionKnapsackToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/knapsack_qubo.rs b/src/rules/knapsack_qubo.rs index fa4c4d973..d84b6bf68 100644 --- a/src/rules/knapsack_qubo.rs +++ b/src/rules/knapsack_qubo.rs @@ -34,6 +34,8 @@ impl ReductionResult for ReductionKnapsackToQUBO { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_items].to_vec()) } } diff --git a/src/rules/ksatisfiability_acyclicpartition.rs b/src/rules/ksatisfiability_acyclicpartition.rs index c93c296fe..8b074ca22 100644 --- a/src/rules/ksatisfiability_acyclicpartition.rs +++ b/src/rules/ksatisfiability_acyclicpartition.rs @@ -103,21 +103,16 @@ impl ReductionResult for ReductionPartitionToAcyclicPartition { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - if target_solution.len() != self.source_num_elements + 2 { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} partition labels, got {}", - self.source_num_elements + 2, - target_solution.len() - ))); - } + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let source_label = target_solution[self.source_vertex]; let sink_label = target_solution[self.sink_vertex]; - debug_assert_ne!( - source_label, sink_label, - "valid target witnesses must place source and sink in different blocks" - ); + if source_label == sink_label { + return Err(crate::rules::ExtractionError::invalid( + "target partition places the source and sink in the same block", + )); + } (0..self.source_num_elements) .map(|item| usize::from(target_solution[item] == sink_label)) @@ -146,6 +141,8 @@ impl ReductionResult for Reduction3SATToAcyclicPartition { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let partition_solution = self .partition_to_acyclic diff --git a/src/rules/ksatisfiability_bicliquecover.rs b/src/rules/ksatisfiability_bicliquecover.rs index 806234010..dd82220fe 100644 --- a/src/rules/ksatisfiability_bicliquecover.rs +++ b/src/rules/ksatisfiability_bicliquecover.rs @@ -102,22 +102,11 @@ impl ReductionResult for ReductionKSatisfiabilityToBicliqueCover { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + let n = self.normalized_n; let left_size = self.target.left_size(); let k = self.target.k(); - let expected_len = (left_size + self.target.right_size()) * k; - if target_solution.len() != expected_len { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {expected_len} biclique-membership values, got {}", - target_solution.len() - ))); - } - if target_solution.iter().any(|&value| value > 1) { - return Err(crate::rules::ExtractionError::invalid( - "biclique-membership values must be binary", - )); - } - // Unified-vertex helpers for the named gadget anchors. let s11_u = self.s1_left_offset; // s_{1,1}^u let s11_v = left_size + self.s1_right_offset; // s_{1,1}^v diff --git a/src/rules/ksatisfiability_cyclicordering.rs b/src/rules/ksatisfiability_cyclicordering.rs index e67b1f7e6..86cb118a6 100644 --- a/src/rules/ksatisfiability_cyclicordering.rs +++ b/src/rules/ksatisfiability_cyclicordering.rs @@ -34,6 +34,8 @@ impl ReductionResult for Reduction3SATToCyclicOrdering { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ (0..self.source_num_vars) .map(|var_idx| { diff --git a/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs b/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs index fbd7c60b4..f5acd64e2 100644 --- a/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs +++ b/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs @@ -175,18 +175,12 @@ impl ReductionResult for Reduction3SATToDirectedTwoCommodityIntegralFlow { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ self.variable_paths .iter() - .map(|paths| { - usize::from( - target_solution - .get(paths.lower_entry_arc) - .copied() - .unwrap_or(0) - > 0, - ) - }) + .map(|paths| usize::from(target_solution[paths.lower_entry_arc] > 0)) .collect() }) } diff --git a/src/rules/ksatisfiability_feasibleregisterassignment.rs b/src/rules/ksatisfiability_feasibleregisterassignment.rs index 07bcd6f31..ccfbb7e11 100644 --- a/src/rules/ksatisfiability_feasibleregisterassignment.rs +++ b/src/rules/ksatisfiability_feasibleregisterassignment.rs @@ -73,6 +73,8 @@ impl ReductionResult for Reduction3SATToFeasibleRegisterAssignment { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ (0..self.num_vars) .map(|var| { diff --git a/src/rules/ksatisfiability_kclique.rs b/src/rules/ksatisfiability_kclique.rs index 994420f7f..1bd049466 100644 --- a/src/rules/ksatisfiability_kclique.rs +++ b/src/rules/ksatisfiability_kclique.rs @@ -40,6 +40,8 @@ impl ReductionResult for Reduction3SATToKClique { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.source_num_vars; // Start with all variables unset (false = 0). diff --git a/src/rules/ksatisfiability_kernel.rs b/src/rules/ksatisfiability_kernel.rs index 02b2568f5..2ba1b794d 100644 --- a/src/rules/ksatisfiability_kernel.rs +++ b/src/rules/ksatisfiability_kernel.rs @@ -29,9 +29,11 @@ impl ReductionResult for Reduction3SatToKernel { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ (0..self.source_num_vars) - .map(|i| usize::from(target_solution.get(2 * i).copied().unwrap_or(0) == 1)) + .map(|i| usize::from(target_solution[2 * i] == 1)) .collect() }) } diff --git a/src/rules/ksatisfiability_minimumvertexcover.rs b/src/rules/ksatisfiability_minimumvertexcover.rs index c3d7faa62..43a33e0cd 100644 --- a/src/rules/ksatisfiability_minimumvertexcover.rs +++ b/src/rules/ksatisfiability_minimumvertexcover.rs @@ -44,6 +44,8 @@ impl ReductionResult for Reduction3SATToMVC { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ (0..self.source_num_vars) .map(|i| { diff --git a/src/rules/ksatisfiability_monochromatictriangle.rs b/src/rules/ksatisfiability_monochromatictriangle.rs index 1c756d1ef..345d72b38 100644 --- a/src/rules/ksatisfiability_monochromatictriangle.rs +++ b/src/rules/ksatisfiability_monochromatictriangle.rs @@ -51,15 +51,12 @@ impl ReductionResult for Reduction3SATToMonochromaticTriangle { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + let direct: Vec = self .negation_edge_indices .iter() - .map( - |&edge_idx| match target_solution.get(edge_idx).copied().unwrap_or(1) { - 0 => 1, - _ => 0, - }, - ) + .map(|&edge_idx| usize::from(target_solution[edge_idx] == 0)) .collect(); if self.source.evaluate(&direct).0 { return Ok(direct); diff --git a/src/rules/ksatisfiability_oneinthreesatisfiability.rs b/src/rules/ksatisfiability_oneinthreesatisfiability.rs index afd4180c5..b26034708 100644 --- a/src/rules/ksatisfiability_oneinthreesatisfiability.rs +++ b/src/rules/ksatisfiability_oneinthreesatisfiability.rs @@ -23,6 +23,8 @@ impl ReductionResult for Reduction3SATToOneInThreeSAT { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.source_num_vars].to_vec()) } } diff --git a/src/rules/ksatisfiability_preemptivescheduling.rs b/src/rules/ksatisfiability_preemptivescheduling.rs index b4df7c385..406b69bb9 100644 --- a/src/rules/ksatisfiability_preemptivescheduling.rs +++ b/src/rules/ksatisfiability_preemptivescheduling.rs @@ -339,6 +339,8 @@ impl ReductionResult for Reduction3SATToPreemptiveScheduling { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let d_max = self.target.d_max(); self.positive_start_jobs diff --git a/src/rules/ksatisfiability_quadraticcongruences.rs b/src/rules/ksatisfiability_quadraticcongruences.rs index d4c635559..cf39c2aa3 100644 --- a/src/rules/ksatisfiability_quadraticcongruences.rs +++ b/src/rules/ksatisfiability_quadraticcongruences.rs @@ -35,6 +35,8 @@ impl ReductionResult for Reduction3SATToQuadraticCongruences { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let mut source_assignment = vec![0; self.source_num_vars]; let Some(x) = self.target.decode_witness(target_solution) else { @@ -62,10 +64,12 @@ impl ReductionResult for Reduction3SATToQuadraticCongruences { for (active_index, &source_index) in self.active_to_source.iter().enumerate() { let alpha_index = 2 * self.standard_clause_count + active_index + 1; - source_assignment[source_index] = if alpha.get(alpha_index) == Some(&-1) { - 1 - } else { - 0 + source_assignment[source_index] = match alpha[alpha_index] { + 1 => 0, + -1 => 1, + sign => return Err(crate::rules::ExtractionError::invalid(format!( + "target witness encodes invalid sign {sign} for source variable {source_index}" + ))), }; } diff --git a/src/rules/ksatisfiability_quadraticdiophantineequations.rs b/src/rules/ksatisfiability_quadraticdiophantineequations.rs index bff64c52e..4fa64bd24 100644 --- a/src/rules/ksatisfiability_quadraticdiophantineequations.rs +++ b/src/rules/ksatisfiability_quadraticdiophantineequations.rs @@ -32,6 +32,8 @@ impl ReductionResult for Reduction3SATToQuadraticDiophantineEquations { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let Some(x) = self.target.decode_witness(target_solution) else { return Err(crate::rules::ExtractionError::invalid( diff --git a/src/rules/ksatisfiability_qubo.rs b/src/rules/ksatisfiability_qubo.rs index 7233435a5..3c2ab369d 100644 --- a/src/rules/ksatisfiability_qubo.rs +++ b/src/rules/ksatisfiability_qubo.rs @@ -36,6 +36,8 @@ impl ReductionResult for ReductionKSatToQUBO { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.source_num_vars].to_vec()) } } @@ -59,6 +61,8 @@ impl ReductionResult for Reduction3SATToQUBO { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.source_num_vars].to_vec()) } } diff --git a/src/rules/ksatisfiability_registersufficiency.rs b/src/rules/ksatisfiability_registersufficiency.rs index d342553b1..ecbb61f94 100644 --- a/src/rules/ksatisfiability_registersufficiency.rs +++ b/src/rules/ksatisfiability_registersufficiency.rs @@ -203,6 +203,8 @@ impl ReductionResult for Reduction3SATToRegisterSufficiency { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ if self.layout.num_vars == 0 { return Ok(Vec::new()); @@ -213,13 +215,15 @@ impl ReductionResult for Reduction3SATToRegisterSufficiency { .map(|var| { let x_pos_before = target_solution[self.layout.x_pos(var)] < cutoff; let x_neg_before = target_solution[self.layout.x_neg(var)] < cutoff; - debug_assert!( - !(x_pos_before && x_neg_before), - "Sethi extraction expects at most one of x_pos/x_neg before w[n]", - ); - usize::from(x_pos_before) + if x_pos_before && x_neg_before { + Err(crate::rules::ExtractionError::invalid(format!( + "both literals of variable {var} precede the extraction cutoff" + ))) + } else { + Ok(usize::from(x_pos_before)) + } }) - .collect() + .collect::>>()? }) } } diff --git a/src/rules/ksatisfiability_simultaneousincongruences.rs b/src/rules/ksatisfiability_simultaneousincongruences.rs index 7e9d1a8bf..f75bdea8c 100644 --- a/src/rules/ksatisfiability_simultaneousincongruences.rs +++ b/src/rules/ksatisfiability_simultaneousincongruences.rs @@ -31,8 +31,10 @@ impl ReductionResult for Reduction3SATToSimultaneousIncongruences { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ - let x = target_solution.first().copied().unwrap_or(0) as u64; + let x = target_solution[0] as u64; self.variable_primes .iter() .map(|&prime| if x % prime == 1 { 1 } else { 0 }) diff --git a/src/rules/ksatisfiability_subsetsum.rs b/src/rules/ksatisfiability_subsetsum.rs index 1f4d575c5..6fb792b97 100644 --- a/src/rules/ksatisfiability_subsetsum.rs +++ b/src/rules/ksatisfiability_subsetsum.rs @@ -39,6 +39,8 @@ impl ReductionResult for Reduction3SATToSubsetSum { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // Variable integers are the first 2n elements in 0-based indexing: // for variable i (0 <= i < n), y_i is stored at index 2*i and z_i at index 2*i + 1. diff --git a/src/rules/ksatisfiability_timetabledesign.rs b/src/rules/ksatisfiability_timetabledesign.rs index 23517ff36..d7a005898 100644 --- a/src/rules/ksatisfiability_timetabledesign.rs +++ b/src/rules/ksatisfiability_timetabledesign.rs @@ -749,6 +749,8 @@ impl ReductionResult for Reduction3SATToTimetableDesign { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let num_tasks = self.target.num_tasks(); let num_periods = self.target.num_periods(); diff --git a/src/rules/lengthboundeddisjointpaths_ilp.rs b/src/rules/lengthboundeddisjointpaths_ilp.rs index 37cacb4e5..2507515d4 100644 --- a/src/rules/lengthboundeddisjointpaths_ilp.rs +++ b/src/rules/lengthboundeddisjointpaths_ilp.rs @@ -36,6 +36,8 @@ impl ReductionResult for ReductionLBDPToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // For each path slot k, set the source vertex-indicator block to 1 // exactly on the vertices incident to the commodity-k path, including s and t. diff --git a/src/rules/longestcircuit_ilp.rs b/src/rules/longestcircuit_ilp.rs index 47f733d2b..7f3b6890c 100644 --- a/src/rules/longestcircuit_ilp.rs +++ b/src/rules/longestcircuit_ilp.rs @@ -39,6 +39,8 @@ impl ReductionResult for ReductionLongestCircuitToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_edges].to_vec()) } } diff --git a/src/rules/longestcommonsubsequence_ilp.rs b/src/rules/longestcommonsubsequence_ilp.rs index b840018a2..924f72227 100644 --- a/src/rules/longestcommonsubsequence_ilp.rs +++ b/src/rules/longestcommonsubsequence_ilp.rs @@ -35,19 +35,14 @@ impl ReductionResult for ReductionLCSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let num_symbols = self.alphabet_size + 1; - let mut witness = Vec::with_capacity(self.max_length); - for position in 0..self.max_length { - let selected = (0..num_symbols) - .find(|&symbol| { - target_solution.get(position * num_symbols + symbol) == Some(&1) - }) - .unwrap_or(self.alphabet_size); - witness.push(selected); - } - witness - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.max_length, + self.alphabet_size + 1, + 0, + ) } } diff --git a/src/rules/longestcommonsubsequence_maximumindependentset.rs b/src/rules/longestcommonsubsequence_maximumindependentset.rs index bcb89bcf7..3571aa771 100644 --- a/src/rules/longestcommonsubsequence_maximumindependentset.rs +++ b/src/rules/longestcommonsubsequence_maximumindependentset.rs @@ -52,6 +52,8 @@ impl ReductionResult for ReductionLCSToIS { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // Collect selected match nodes with their characters let mut selected: Vec<(usize, usize)> = target_solution diff --git a/src/rules/longestpath_ilp.rs b/src/rules/longestpath_ilp.rs index 28b8e41de..5143f8baf 100644 --- a/src/rules/longestpath_ilp.rs +++ b/src/rules/longestpath_ilp.rs @@ -35,20 +35,14 @@ impl ReductionResult for ReductionLongestPathToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ (0..self.num_edges) .map(|edge_idx| { usize::from( - target_solution - .get(Self::arc_var(edge_idx, 0)) - .copied() - .unwrap_or(0) - > 0 - || target_solution - .get(Self::arc_var(edge_idx, 1)) - .copied() - .unwrap_or(0) - > 0, + target_solution[Self::arc_var(edge_idx, 0)] > 0 + || target_solution[Self::arc_var(edge_idx, 1)] > 0, ) }) .collect() diff --git a/src/rules/maxcut_minimumcutintoboundedsets.rs b/src/rules/maxcut_minimumcutintoboundedsets.rs index 72dc2c678..3b95ec4b6 100644 --- a/src/rules/maxcut_minimumcutintoboundedsets.rs +++ b/src/rules/maxcut_minimumcutintoboundedsets.rs @@ -34,6 +34,8 @@ impl ReductionResult for ReductionMaxCutToMinCutBounded { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.original_n].to_vec()) } } diff --git a/src/rules/maxcut_minimummatrixcover.rs b/src/rules/maxcut_minimummatrixcover.rs index c577dbd97..080f1d924 100644 --- a/src/rules/maxcut_minimummatrixcover.rs +++ b/src/rules/maxcut_minimummatrixcover.rs @@ -52,6 +52,8 @@ impl ReductionResult for ReductionMaxCutToMMC { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/maximalis_ilp.rs b/src/rules/maximalis_ilp.rs index abb063b50..c77f8578f 100644 --- a/src/rules/maximalis_ilp.rs +++ b/src/rules/maximalis_ilp.rs @@ -26,6 +26,8 @@ impl ReductionResult for ReductionMxISToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/maximum2satisfiability_ilp.rs b/src/rules/maximum2satisfiability_ilp.rs index 8d2cdbb62..92965836a 100644 --- a/src/rules/maximum2satisfiability_ilp.rs +++ b/src/rules/maximum2satisfiability_ilp.rs @@ -31,6 +31,8 @@ impl ReductionResult for ReductionMaximum2SatisfiabilityToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_vars].to_vec()) } } diff --git a/src/rules/maximum2satisfiability_maxcut.rs b/src/rules/maximum2satisfiability_maxcut.rs index f2e5ddfc1..06b12ee6d 100644 --- a/src/rules/maximum2satisfiability_maxcut.rs +++ b/src/rules/maximum2satisfiability_maxcut.rs @@ -37,6 +37,8 @@ impl ReductionResult for ReductionMaximum2SatisfiabilityToMaxCut { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let reference_side = target_solution[0]; (0..self.source_num_vars) diff --git a/src/rules/maximumclique_ilp.rs b/src/rules/maximumclique_ilp.rs index 145c2b506..1021cd4b4 100644 --- a/src/rules/maximumclique_ilp.rs +++ b/src/rules/maximumclique_ilp.rs @@ -39,6 +39,8 @@ impl ReductionResult for ReductionCliqueToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/maximumclique_maximumindependentset.rs b/src/rules/maximumclique_maximumindependentset.rs index 6d03be0bb..2a5780cbd 100644 --- a/src/rules/maximumclique_maximumindependentset.rs +++ b/src/rules/maximumclique_maximumindependentset.rs @@ -32,6 +32,8 @@ where &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/maximumcokplex_ilp.rs b/src/rules/maximumcokplex_ilp.rs index 9cc1751c3..90b56cbc1 100644 --- a/src/rules/maximumcokplex_ilp.rs +++ b/src/rules/maximumcokplex_ilp.rs @@ -35,6 +35,8 @@ where &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/maximumcommonedgesubgraph_ilp.rs b/src/rules/maximumcommonedgesubgraph_ilp.rs index 2f1df2648..e64b7b139 100644 --- a/src/rules/maximumcommonedgesubgraph_ilp.rs +++ b/src/rules/maximumcommonedgesubgraph_ilp.rs @@ -47,17 +47,22 @@ impl ReductionResult for ReductionMCESToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let n1 = self.num_vertices_1; - let n2 = self.num_vertices_2; - (0..n1) - .map(|u| { - (0..n2) - .find(|&p| target_solution[u * n2 + p] == 1) - .unwrap_or(n2) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + let n2 = self.num_vertices_2; + (0..self.num_vertices_1) + .map(|vertex| { + let mut selected = + (0..n2).filter(|&mapped| target_solution[vertex * n2 + mapped] == 1); + match (selected.next(), selected.next()) { + (Some(mapped), None) => Ok(mapped), + (None, _) => Ok(n2), + (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( + "source vertex {vertex} maps to multiple target vertices" + ))), + } + }) + .collect() } } diff --git a/src/rules/maximumcontactmapoverlap_ilp.rs b/src/rules/maximumcontactmapoverlap_ilp.rs index b666fe801..39c08a3a7 100644 --- a/src/rules/maximumcontactmapoverlap_ilp.rs +++ b/src/rules/maximumcontactmapoverlap_ilp.rs @@ -50,18 +50,22 @@ impl ReductionResult for ReductionCMOToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let n1 = self.num_vertices_1; - let n2 = self.num_vertices_2; - (0..n1) - .map(|i| { - (0..n2) - .find(|&j| target_solution[i * n2 + j] == 1) - .map(|j| j + 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + let n2 = self.num_vertices_2; + (0..self.num_vertices_1) + .map(|residue| { + let mut selected = + (0..n2).filter(|&mapped| target_solution[residue * n2 + mapped] == 1); + match (selected.next(), selected.next()) { + (Some(mapped), None) => Ok(mapped + 1), + (None, _) => Ok(0), + (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( + "source residue {residue} maps to multiple target residues" + ))), + } + }) + .collect() } } diff --git a/src/rules/maximumdomaticnumber_ilp.rs b/src/rules/maximumdomaticnumber_ilp.rs index 494f62716..1f2f9b0b4 100644 --- a/src/rules/maximumdomaticnumber_ilp.rs +++ b/src/rules/maximumdomaticnumber_ilp.rs @@ -40,6 +40,8 @@ impl ReductionResult for ReductionDomaticNumberToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.n; let mut config = vec![0; n]; diff --git a/src/rules/maximumedgeweightedkclique_ilp.rs b/src/rules/maximumedgeweightedkclique_ilp.rs index c7a0d42e9..32e814eed 100644 --- a/src/rules/maximumedgeweightedkclique_ilp.rs +++ b/src/rules/maximumedgeweightedkclique_ilp.rs @@ -62,6 +62,8 @@ where &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_vertices].to_vec()) } } diff --git a/src/rules/maximumindependentset_gridgraph.rs b/src/rules/maximumindependentset_gridgraph.rs index 36cf30bd2..9330392bc 100644 --- a/src/rules/maximumindependentset_gridgraph.rs +++ b/src/rules/maximumindependentset_gridgraph.rs @@ -29,6 +29,8 @@ impl ReductionResult for ReductionISSimpleOneToGridOne { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(self.mapping_result.map_config_back(target_solution)) } } diff --git a/src/rules/maximumindependentset_integralflowbundles.rs b/src/rules/maximumindependentset_integralflowbundles.rs index 8699ac72d..1f27a8f7b 100644 --- a/src/rules/maximumindependentset_integralflowbundles.rs +++ b/src/rules/maximumindependentset_integralflowbundles.rs @@ -47,15 +47,11 @@ impl ReductionResult for ReductionMISToIFB { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ (0..self.num_source_vertices) - .map(|i| { - if target_solution.get(2 * i + 1).copied().unwrap_or(0) > 0 { - 1 - } else { - 0 - } - }) + .map(|i| if target_solution[2 * i + 1] > 0 { 1 } else { 0 }) .collect() }) } diff --git a/src/rules/maximumindependentset_maximumclique.rs b/src/rules/maximumindependentset_maximumclique.rs index 701d6ab2e..0bd89db62 100644 --- a/src/rules/maximumindependentset_maximumclique.rs +++ b/src/rules/maximumindependentset_maximumclique.rs @@ -32,6 +32,8 @@ where &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/maximumindependentset_maximumsetpacking.rs b/src/rules/maximumindependentset_maximumsetpacking.rs index 62b575a6b..4811bb8ac 100644 --- a/src/rules/maximumindependentset_maximumsetpacking.rs +++ b/src/rules/maximumindependentset_maximumsetpacking.rs @@ -33,6 +33,8 @@ where &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } @@ -87,6 +89,8 @@ where &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/maximumindependentset_triangular.rs b/src/rules/maximumindependentset_triangular.rs index d83489aef..063416825 100644 --- a/src/rules/maximumindependentset_triangular.rs +++ b/src/rules/maximumindependentset_triangular.rs @@ -31,6 +31,8 @@ impl ReductionResult for ReductionISSimpleToTriangular { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ self.mapping_result .map_config_back_via_centers(target_solution) diff --git a/src/rules/maximumleafspanningtree_ilp.rs b/src/rules/maximumleafspanningtree_ilp.rs index e29c034ca..eb31cf93f 100644 --- a/src/rules/maximumleafspanningtree_ilp.rs +++ b/src/rules/maximumleafspanningtree_ilp.rs @@ -43,6 +43,8 @@ impl ReductionResult for ReductionMaximumLeafSpanningTreeToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // First m variables are edge selectors target_solution[..self.num_edges].to_vec() diff --git a/src/rules/maximumlikelihoodranking_ilp.rs b/src/rules/maximumlikelihoodranking_ilp.rs index fe0525792..2a5276546 100644 --- a/src/rules/maximumlikelihoodranking_ilp.rs +++ b/src/rules/maximumlikelihoodranking_ilp.rs @@ -43,6 +43,8 @@ impl ReductionResult for ReductionMaximumLikelihoodRankingToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.n; if n == 0 { diff --git a/src/rules/maximummatching_ilp.rs b/src/rules/maximummatching_ilp.rs index 840b817fe..a806a5716 100644 --- a/src/rules/maximummatching_ilp.rs +++ b/src/rules/maximummatching_ilp.rs @@ -39,6 +39,8 @@ impl ReductionResult for ReductionMatchingToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/maximummatching_maximumsetpacking.rs b/src/rules/maximummatching_maximumsetpacking.rs index da3161860..f2c58a57a 100644 --- a/src/rules/maximummatching_maximumsetpacking.rs +++ b/src/rules/maximummatching_maximumsetpacking.rs @@ -34,6 +34,8 @@ where &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/maximumsetpacking_ilp.rs b/src/rules/maximumsetpacking_ilp.rs index c464fc9a8..975cc4428 100644 --- a/src/rules/maximumsetpacking_ilp.rs +++ b/src/rules/maximumsetpacking_ilp.rs @@ -33,6 +33,8 @@ impl ReductionResult for ReductionSPToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/maximumsetpacking_qubo.rs b/src/rules/maximumsetpacking_qubo.rs index 901d7f7f2..4e13970a4 100644 --- a/src/rules/maximumsetpacking_qubo.rs +++ b/src/rules/maximumsetpacking_qubo.rs @@ -29,6 +29,8 @@ impl ReductionResult for ReductionSPToQUBO { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/minimumcapacitatedspanningtree_ilp.rs b/src/rules/minimumcapacitatedspanningtree_ilp.rs index 55854846b..60f748f0a 100644 --- a/src/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/rules/minimumcapacitatedspanningtree_ilp.rs @@ -46,6 +46,8 @@ impl ReductionResult for ReductionMinimumCapacitatedSpanningTreeToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // First m variables are edge selectors target_solution[..self.num_edges].to_vec() diff --git a/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs b/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs index 7f90a1780..6a52e05c2 100644 --- a/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs +++ b/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs @@ -47,6 +47,8 @@ impl ReductionResult for ReductionMCMFToMCC { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_original_arcs].to_vec()) } } diff --git a/src/rules/minimumcoveringbycliques_ilp.rs b/src/rules/minimumcoveringbycliques_ilp.rs index 7f9fcf584..96e54b719 100644 --- a/src/rules/minimumcoveringbycliques_ilp.rs +++ b/src/rules/minimumcoveringbycliques_ilp.rs @@ -43,21 +43,21 @@ impl ReductionResult for ReductionMinimumCoveringByCliquesToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - if self.num_edges == 0 { - return Ok(vec![]); - } - - (0..self.num_edges) - .map(|edge_idx| { - (0..self.num_edges) - .find(|&slot| { - target_solution[self.y_offset + edge_idx * self.num_edges + slot] == 1 - }) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + (0..self.num_edges) + .map(|edge| { + (0..self.num_edges) + .find(|&clique| { + target_solution[self.y_offset + edge * self.num_edges + clique] == 1 + }) + .ok_or_else(|| { + crate::rules::ExtractionError::invalid(format!( + "edge {edge} is not covered by any clique" + )) + }) + }) + .collect() } } diff --git a/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs b/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs index cd905db87..c6f6ba7a1 100644 --- a/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs +++ b/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs @@ -84,6 +84,8 @@ impl ReductionResult for ReductionMinimumCoveringByCliquesToMinimumIntersectionG &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ if !self.target.evaluate(target_solution).is_valid() { return Err(crate::rules::ExtractionError::invalid( diff --git a/src/rules/minimumcutintoboundedsets_ilp.rs b/src/rules/minimumcutintoboundedsets_ilp.rs index 44c29cd66..654642c15 100644 --- a/src/rules/minimumcutintoboundedsets_ilp.rs +++ b/src/rules/minimumcutintoboundedsets_ilp.rs @@ -30,6 +30,8 @@ impl ReductionResult for ReductionMinCutBSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_vertices].to_vec()) } } diff --git a/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs b/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs index e99f9817d..17e793f45 100644 --- a/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs +++ b/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs @@ -43,18 +43,28 @@ impl ReductionResult for ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - self.block_offsets - .iter() - .zip(&self.block_sizes) - .map(|(&start, &size)| { - target_solution[start..start + size] - .iter() - .position(|&bit| bit == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + self.block_offsets + .iter() + .zip(&self.block_sizes) + .enumerate() + .map(|(link, (&start, &size))| { + let mut selected = target_solution[start..start + size] + .iter() + .enumerate() + .filter_map(|(orientation, &bit)| (bit == 1).then_some(orientation)); + match (selected.next(), selected.next()) { + (Some(orientation), None) => Ok(orientation), + (None, _) => Err(crate::rules::ExtractionError::invalid(format!( + "link {link} has no selected orientation" + ))), + (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( + "link {link} has multiple selected orientations" + ))), + } + }) + .collect() } } diff --git a/src/rules/minimumdominatingset_ilp.rs b/src/rules/minimumdominatingset_ilp.rs index 4d46d094c..78a891024 100644 --- a/src/rules/minimumdominatingset_ilp.rs +++ b/src/rules/minimumdominatingset_ilp.rs @@ -40,6 +40,8 @@ impl ReductionResult for ReductionDSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/minimumedgecostflow_ilp.rs b/src/rules/minimumedgecostflow_ilp.rs index 206a1ec33..e1fe7557d 100644 --- a/src/rules/minimumedgecostflow_ilp.rs +++ b/src/rules/minimumedgecostflow_ilp.rs @@ -47,6 +47,8 @@ impl ReductionResult for ReductionMECFToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_edges].to_vec()) } } diff --git a/src/rules/minimumexternalmacrodatacompression_ilp.rs b/src/rules/minimumexternalmacrodatacompression_ilp.rs index 042943139..745bc9458 100644 --- a/src/rules/minimumexternalmacrodatacompression_ilp.rs +++ b/src/rules/minimumexternalmacrodatacompression_ilp.rs @@ -125,6 +125,8 @@ impl ReductionResult for ReductionEMDCToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.layout.n; let k = self.alphabet_size; @@ -133,13 +135,27 @@ impl ReductionResult for ReductionEMDCToILP { // Build D-slots let mut d_slots = vec![empty; n]; for j in 0..n { + let symbols: Vec<_> = (0..k) + .filter(|&c| target_solution[self.layout.d_var(j, c)] == 1) + .collect(); if target_solution[self.layout.d_used_var(j)] == 1 { - for c in 0..k { - if target_solution[self.layout.d_var(j, c)] == 1 { - d_slots[j] = c; - break; + match symbols.as_slice() { + [symbol] => d_slots[j] = *symbol, + [] => { + return Err(crate::rules::ExtractionError::invalid(format!( + "dictionary slot {j} is active without a symbol" + ))) + } + _ => { + return Err(crate::rules::ExtractionError::invalid(format!( + "dictionary slot {j} selects multiple symbols" + ))) } } + } else if !symbols.is_empty() { + return Err(crate::rules::ExtractionError::invalid(format!( + "inactive dictionary slot {j} selects a symbol" + ))); } } @@ -148,37 +164,35 @@ impl ReductionResult for ReductionEMDCToILP { let mut c_pos = 0; let mut pos = 0; while pos < n { - // Check if lit[pos] = 1 + let pointers: Vec<_> = (1..=(n - pos)) + .flat_map(|length| { + (0..=(n - length)).filter_map(move |start| { + (target_solution[self.layout.ptr_var(pos, length, start)] == 1) + .then_some((start, length)) + }) + }) + .collect(); if target_solution[self.layout.lit_var(pos)] == 1 { + if !pointers.is_empty() { + return Err(crate::rules::ExtractionError::invalid(format!( + "position {pos} selects both a literal and a pointer" + ))); + } // Literal at position pos c_slots[c_pos] = self.source_string[pos]; c_pos += 1; pos += 1; continue; } - // Check for an active pointer starting at pos - let mut found = false; - for l in 1..=(n - pos) { - for d_start in 0..=(n - l) { - let var_idx = self.layout.ptr_var(pos, l, d_start); - if target_solution[var_idx] == 1 { - // Encode pointer (d_start, l) as EMDC pointer index - let ptr_idx = encode_pointer(n, d_start, l); - c_slots[c_pos] = k + 1 + ptr_idx; - c_pos += 1; - pos += l; - found = true; - break; - } - } - if found { - break; - } - } - if !found { - // Should not happen with a valid ILP solution - pos += 1; - } + let [(d_start, length)] = pointers.as_slice() else { + return Err(crate::rules::ExtractionError::invalid(format!( + "position {pos} must select exactly one pointer" + ))); + }; + let ptr_idx = encode_pointer(n, *d_start, *length); + c_slots[c_pos] = k + 1 + ptr_idx; + c_pos += 1; + pos += length; } // Combine D-slots and C-slots diff --git a/src/rules/minimumfaultdetectiontestset_ilp.rs b/src/rules/minimumfaultdetectiontestset_ilp.rs index d412ff0e3..489a1e00a 100644 --- a/src/rules/minimumfaultdetectiontestset_ilp.rs +++ b/src/rules/minimumfaultdetectiontestset_ilp.rs @@ -29,6 +29,8 @@ impl ReductionResult for ReductionMFDTSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/minimumfeedbackarcset_ilp.rs b/src/rules/minimumfeedbackarcset_ilp.rs index 58d65af24..bd36b5d4c 100644 --- a/src/rules/minimumfeedbackarcset_ilp.rs +++ b/src/rules/minimumfeedbackarcset_ilp.rs @@ -45,6 +45,8 @@ impl ReductionResult for ReductionFASToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_arcs].to_vec()) } } diff --git a/src/rules/minimumfeedbackarcset_maximumlikelihoodranking.rs b/src/rules/minimumfeedbackarcset_maximumlikelihoodranking.rs index e5e493698..29b5cfa35 100644 --- a/src/rules/minimumfeedbackarcset_maximumlikelihoodranking.rs +++ b/src/rules/minimumfeedbackarcset_maximumlikelihoodranking.rs @@ -52,6 +52,8 @@ impl ReductionResult for ReductionFASToMLR { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ self.source_arcs .iter() diff --git a/src/rules/minimumfeedbackvertexset_ilp.rs b/src/rules/minimumfeedbackvertexset_ilp.rs index 5ceaac91d..8393c97c2 100644 --- a/src/rules/minimumfeedbackvertexset_ilp.rs +++ b/src/rules/minimumfeedbackvertexset_ilp.rs @@ -42,6 +42,8 @@ impl ReductionResult for ReductionMFVSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_vertices].to_vec()) } } diff --git a/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs b/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs index 397d5dfe0..08d5be031 100644 --- a/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs +++ b/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs @@ -41,6 +41,8 @@ impl ReductionResult for ReductionFVSToCodeGen { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_source_vertices; let mut source_config = vec![0usize; n]; diff --git a/src/rules/minimumgraphbandwidth_ilp.rs b/src/rules/minimumgraphbandwidth_ilp.rs index 33e0c6aaa..cdfd3a5a9 100644 --- a/src/rules/minimumgraphbandwidth_ilp.rs +++ b/src/rules/minimumgraphbandwidth_ilp.rs @@ -38,16 +38,14 @@ impl ReductionResult for ReductionMGBToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let n = self.num_vertices; - (0..n) - .map(|v| { - (0..n) - .find(|&p| target_solution[v * n + p] == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.num_vertices, + self.num_vertices, + 0, + ) } } diff --git a/src/rules/minimumhittingset_ilp.rs b/src/rules/minimumhittingset_ilp.rs index 3940752c4..06d81bda6 100644 --- a/src/rules/minimumhittingset_ilp.rs +++ b/src/rules/minimumhittingset_ilp.rs @@ -25,6 +25,8 @@ impl ReductionResult for ReductionHSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/minimuminternalmacrodatacompression_ilp.rs b/src/rules/minimuminternalmacrodatacompression_ilp.rs index 9d9d68d7a..21f837e64 100644 --- a/src/rules/minimuminternalmacrodatacompression_ilp.rs +++ b/src/rules/minimuminternalmacrodatacompression_ilp.rs @@ -99,6 +99,8 @@ impl ReductionResult for ReductionIMDCToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.layout.n; let k = self.alphabet_size; diff --git a/src/rules/minimummatrixcover_ilp.rs b/src/rules/minimummatrixcover_ilp.rs index bd23fbee6..7375a2beb 100644 --- a/src/rules/minimummatrixcover_ilp.rs +++ b/src/rules/minimummatrixcover_ilp.rs @@ -31,6 +31,8 @@ impl ReductionResult for ReductionMinimumMatrixCoverToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // First n variables are the sign variables x_0,...,x_{n-1} target_solution[..self.n].to_vec() diff --git a/src/rules/minimummaximalmatching_ilp.rs b/src/rules/minimummaximalmatching_ilp.rs index f99124ed4..3fe992afd 100644 --- a/src/rules/minimummaximalmatching_ilp.rs +++ b/src/rules/minimummaximalmatching_ilp.rs @@ -42,6 +42,8 @@ impl ReductionResult for ReductionMMMToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/minimummaximalmatching_maximumachromaticnumber.rs b/src/rules/minimummaximalmatching_maximumachromaticnumber.rs index eda43212a..1eb8c7953 100644 --- a/src/rules/minimummaximalmatching_maximumachromaticnumber.rs +++ b/src/rules/minimummaximalmatching_maximumachromaticnumber.rs @@ -46,6 +46,8 @@ impl ReductionResult for ReductionMMMToAchromatic { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ self.source_edges .iter() diff --git a/src/rules/minimummaximalmatching_minimummatrixdomination.rs b/src/rules/minimummaximalmatching_minimummatrixdomination.rs index 3909625cc..88bc36e76 100644 --- a/src/rules/minimummaximalmatching_minimummatrixdomination.rs +++ b/src/rules/minimummaximalmatching_minimummatrixdomination.rs @@ -97,6 +97,8 @@ impl ReductionResult for ReductionMMMToMatrixDomination { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let graph = self.source.graph(); let edges = graph.edges(); @@ -125,12 +127,16 @@ impl ReductionResult for ReductionMMMToMatrixDomination { .zip(target_ones.iter()) .filter_map(|(&sel, &cell)| { if sel == 1 { - cell_to_source_edge.get(&cell).copied() + Some(cell_to_source_edge.get(&cell).copied().ok_or_else(|| { + crate::rules::ExtractionError::invalid(format!( + "selected matrix cell {cell:?} has no source edge" + )) + })) } else { None } }) - .collect(); + .collect::>()?; // Step 2: Yannakakis-Gavril EDS -> independent EDS (maximal matching). // Loop invariants: `d` is an EDS of the source graph; each iteration @@ -145,13 +151,23 @@ impl ReductionResult for ReductionMMMToMatrixDomination { // Try dropping e1_idx or e2_idx if the remainder is still an EDS. let mut without_e1 = d.clone(); - without_e1.swap_remove(d.iter().position(|&x| x == e1_idx).unwrap()); + let e1_position = d.iter().position(|&x| x == e1_idx).ok_or_else(|| { + crate::rules::ExtractionError::invalid( + "edge-domination transformation lost its selected edge", + ) + })?; + without_e1.swap_remove(e1_position); if is_edge_dominating_set(&without_e1, &edges) { d = without_e1; continue; } let mut without_e2 = d.clone(); - without_e2.swap_remove(d.iter().position(|&x| x == e2_idx).unwrap()); + let e2_position = d.iter().position(|&x| x == e2_idx).ok_or_else(|| { + crate::rules::ExtractionError::invalid( + "edge-domination transformation lost its selected edge", + ) + })?; + without_e2.swap_remove(e2_position); if is_edge_dominating_set(&without_e2, &edges) { d = without_e2; continue; @@ -173,12 +189,12 @@ impl ReductionResult for ReductionMMMToMatrixDomination { // Try to swap e1 := (u, x) where x ∉ V(d \ {e1}). The YG proof // guarantees such x exists when neither drop succeeded. if let Some(new_idx) = find_swap_edge(u, e1_idx, &d, &edges) { - replace_in(&mut d, e1_idx, new_idx); + d[e1_position] = new_idx; continue; } // Symmetric swap on e2. if let Some(new_idx) = find_swap_edge(w, e2_idx, &d, &edges) { - replace_in(&mut d, e2_idx, new_idx); + d[e2_position] = new_idx; continue; } @@ -186,10 +202,9 @@ impl ReductionResult for ReductionMMMToMatrixDomination { // above succeeds. Reaching this point implies the input was not // a valid EDS (i.e., not a feasible MMD witness on the constructed // instance), which violates the reduction's precondition. - unreachable!( - "Yannakakis-Gavril EDS->IEDS transformation could not progress; \ - target witness must be a feasible (dominating) MMD configuration" - ); + return Err(crate::rules::ExtractionError::invalid( + "target matrix entries do not encode an edge-dominating set", + )); } // Step 3: encode the matching as a binary configuration over source edges. @@ -277,16 +292,6 @@ fn find_swap_edge( None } -/// Replace `old_idx` with `new_idx` inside `d` in-place. Panics if `old_idx` -/// is not present. -fn replace_in(d: &mut [usize], old_idx: usize, new_idx: usize) { - let pos = d - .iter() - .position(|&x| x == old_idx) - .expect("old_idx must be present in d"); - d[pos] = new_idx; -} - #[reduction( overhead = { num_rows = "num_vertices", diff --git a/src/rules/minimummetricdimension_ilp.rs b/src/rules/minimummetricdimension_ilp.rs index 8f0982d03..e190e5458 100644 --- a/src/rules/minimummetricdimension_ilp.rs +++ b/src/rules/minimummetricdimension_ilp.rs @@ -42,6 +42,8 @@ impl ReductionResult for ReductionMDToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/minimummultiwaycut_ilp.rs b/src/rules/minimummultiwaycut_ilp.rs index bb130dd7f..bb6002b41 100644 --- a/src/rules/minimummultiwaycut_ilp.rs +++ b/src/rules/minimummultiwaycut_ilp.rs @@ -46,6 +46,8 @@ impl ReductionResult for ReductionMMCToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let offset = self.k * self.n; (0..self.m).map(|e| target_solution[offset + e]).collect() diff --git a/src/rules/minimummultiwaycut_qubo.rs b/src/rules/minimummultiwaycut_qubo.rs index e29b0c45a..d384602fb 100644 --- a/src/rules/minimummultiwaycut_qubo.rs +++ b/src/rules/minimummultiwaycut_qubo.rs @@ -40,18 +40,15 @@ impl ReductionResult for ReductionMinimumMultiwayCutToQUBO { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let k = self.num_terminals; let n = self.num_vertices; // For each vertex, find which terminal position it is assigned to - let assignments: Vec = (0..n) - .map(|u| { - (0..k) - .find(|&t| target_solution[u * k + t] == 1) - .unwrap_or(0) - }) - .collect(); + let assignments = + crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, n, k, 0)?; // For each edge, output 1 (cut) if endpoints differ, 0 (keep) otherwise self.edges diff --git a/src/rules/minimumsetcovering_ilp.rs b/src/rules/minimumsetcovering_ilp.rs index 2b17f517e..1305e7910 100644 --- a/src/rules/minimumsetcovering_ilp.rs +++ b/src/rules/minimumsetcovering_ilp.rs @@ -37,6 +37,8 @@ impl ReductionResult for ReductionSCToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/minimumsummulticenter_ilp.rs b/src/rules/minimumsummulticenter_ilp.rs index 9e78166df..5bfa28403 100644 --- a/src/rules/minimumsummulticenter_ilp.rs +++ b/src/rules/minimumsummulticenter_ilp.rs @@ -45,6 +45,8 @@ impl ReductionResult for ReductionMSMCToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_vertices].to_vec()) } } diff --git a/src/rules/minimumtardinesssequencing_ilp.rs b/src/rules/minimumtardinesssequencing_ilp.rs index 0c4335ede..5fdeccdbd 100644 --- a/src/rules/minimumtardinesssequencing_ilp.rs +++ b/src/rules/minimumtardinesssequencing_ilp.rs @@ -30,9 +30,11 @@ impl ReductionResult for ReductionMTSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_tasks; - let schedule = one_hot_decode(target_solution, n, n, 0); + let schedule = one_hot_decode(target_solution, n, n, 0)?; permutation_to_lehmer(&schedule) }) } @@ -57,9 +59,11 @@ impl ReductionResult for ReductionMTSWeightedToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_tasks; - let schedule = one_hot_decode(target_solution, n, n, 0); + let schedule = one_hot_decode(target_solution, n, n, 0)?; permutation_to_lehmer(&schedule) }) } diff --git a/src/rules/minimumvertexcover_comparativecontainment.rs b/src/rules/minimumvertexcover_comparativecontainment.rs index 898f1eae9..3b1e13333 100644 --- a/src/rules/minimumvertexcover_comparativecontainment.rs +++ b/src/rules/minimumvertexcover_comparativecontainment.rs @@ -49,14 +49,15 @@ impl ReductionResult for ReductionDecisionMVCToComparativeContainment { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ if let Some(witness) = &self.trivial_yes { return Ok(witness.clone()); } let mut cover = vec![0; self.num_source_vertices]; - for (vertex, &selected) in target_solution + for (vertex, &selected) in target_solution[..self.num_source_vertices] .iter() - .take(self.num_source_vertices) .enumerate() { cover[vertex] = selected; diff --git a/src/rules/minimumvertexcover_ensemblecomputation.rs b/src/rules/minimumvertexcover_ensemblecomputation.rs index 292a57245..c486169fb 100644 --- a/src/rules/minimumvertexcover_ensemblecomputation.rs +++ b/src/rules/minimumvertexcover_ensemblecomputation.rs @@ -49,6 +49,8 @@ impl ReductionResult for ReductionVCToEC { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ use crate::traits::Problem; use crate::types::Min; diff --git a/src/rules/minimumvertexcover_longestcommonsubsequence.rs b/src/rules/minimumvertexcover_longestcommonsubsequence.rs index 324fd5692..d326f12cd 100644 --- a/src/rules/minimumvertexcover_longestcommonsubsequence.rs +++ b/src/rules/minimumvertexcover_longestcommonsubsequence.rs @@ -25,6 +25,8 @@ impl ReductionResult for ReductionVCToLCS { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let mut cover = vec![1; self.num_vertices]; for &symbol in target_solution { diff --git a/src/rules/minimumvertexcover_maximumindependentset.rs b/src/rules/minimumvertexcover_maximumindependentset.rs index 3ed74e3be..791779d9b 100644 --- a/src/rules/minimumvertexcover_maximumindependentset.rs +++ b/src/rules/minimumvertexcover_maximumindependentset.rs @@ -31,6 +31,8 @@ where &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.iter().map(|&x| 1 - x).collect()) } } @@ -75,6 +77,8 @@ where &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.iter().map(|&x| 1 - x).collect()) } } diff --git a/src/rules/minimumvertexcover_minimumfeedbackarcset.rs b/src/rules/minimumvertexcover_minimumfeedbackarcset.rs index f8a45f664..6dc9240a0 100644 --- a/src/rules/minimumvertexcover_minimumfeedbackarcset.rs +++ b/src/rules/minimumvertexcover_minimumfeedbackarcset.rs @@ -35,6 +35,8 @@ impl ReductionResult for ReductionVCToFAS { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_source_vertices].to_vec()) } } diff --git a/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs b/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs index e8af6b26f..b39ef35d6 100644 --- a/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs +++ b/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs @@ -30,6 +30,8 @@ where &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/minimumvertexcover_minimumhittingset.rs b/src/rules/minimumvertexcover_minimumhittingset.rs index 57e9b6ed2..c306a8ca2 100644 --- a/src/rules/minimumvertexcover_minimumhittingset.rs +++ b/src/rules/minimumvertexcover_minimumhittingset.rs @@ -30,6 +30,8 @@ impl ReductionResult for ReductionVCToHS { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/minimumvertexcover_minimumsetcovering.rs b/src/rules/minimumvertexcover_minimumsetcovering.rs index bbff2c664..e7f945fde 100644 --- a/src/rules/minimumvertexcover_minimumsetcovering.rs +++ b/src/rules/minimumvertexcover_minimumsetcovering.rs @@ -33,6 +33,8 @@ where &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/minimumvertexcover_minimumweightandorgraph.rs b/src/rules/minimumvertexcover_minimumweightandorgraph.rs index 5628d979f..dc518f161 100644 --- a/src/rules/minimumvertexcover_minimumweightandorgraph.rs +++ b/src/rules/minimumvertexcover_minimumweightandorgraph.rs @@ -27,9 +27,11 @@ impl ReductionResult for ReductionVCToAndOrGraph { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ (0..self.num_source_vertices) - .map(|j| usize::from(target_solution.get(self.sink_arc_start + j) == Some(&1))) + .map(|j| usize::from(target_solution[self.sink_arc_start + j] == 1)) .collect() }) } diff --git a/src/rules/minimumweightdecoding_ilp.rs b/src/rules/minimumweightdecoding_ilp.rs index daf35aa05..df4698183 100644 --- a/src/rules/minimumweightdecoding_ilp.rs +++ b/src/rules/minimumweightdecoding_ilp.rs @@ -44,6 +44,8 @@ impl ReductionResult for ReductionMinimumWeightDecodingToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_cols].to_vec()) } } diff --git a/src/rules/minmaxmulticenter_ilp.rs b/src/rules/minmaxmulticenter_ilp.rs index eb9ccd79e..e6b67cdcd 100644 --- a/src/rules/minmaxmulticenter_ilp.rs +++ b/src/rules/minmaxmulticenter_ilp.rs @@ -49,6 +49,8 @@ impl ReductionResult for ReductionMMCToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_vertices].to_vec()) } } diff --git a/src/rules/mixedchinesepostman_ilp.rs b/src/rules/mixedchinesepostman_ilp.rs index 173fa5a94..d96c86471 100644 --- a/src/rules/mixedchinesepostman_ilp.rs +++ b/src/rules/mixedchinesepostman_ilp.rs @@ -30,6 +30,8 @@ impl ReductionResult for ReductionMCPToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // Return the orientation bits d_k in source edge order target_solution[..self.num_undirected_edges].to_vec() diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 7a9dafa97..b6ed58db3 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -417,7 +417,7 @@ pub use search::{ ApproximationPolicy, LimitReached, SearchCompleteness, SearchLimits, SearchMode, SearchOutcome, SearchStats, }; -pub(crate) use traits::DynReductionResult; +pub(crate) use traits::{validate_target_solution, DynReductionResult}; pub use traits::{ AggregateReductionResult, ExtractionError, ExtractionResult, ReduceTo, ReduceToAggregate, ReductionAutoCast, ReductionResult, diff --git a/src/rules/monochromatictriangle_ilp.rs b/src/rules/monochromatictriangle_ilp.rs index 4da4805c3..9485e25a4 100644 --- a/src/rules/monochromatictriangle_ilp.rs +++ b/src/rules/monochromatictriangle_ilp.rs @@ -28,6 +28,8 @@ impl ReductionResult for ReductionMonochromaticTriangleToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/multiplecopyfileallocation_ilp.rs b/src/rules/multiplecopyfileallocation_ilp.rs index 87c238c6c..8d0194dc0 100644 --- a/src/rules/multiplecopyfileallocation_ilp.rs +++ b/src/rules/multiplecopyfileallocation_ilp.rs @@ -40,6 +40,8 @@ impl ReductionResult for ReductionMCFAToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_vertices].to_vec()) } } diff --git a/src/rules/multiprocessorscheduling_ilp.rs b/src/rules/multiprocessorscheduling_ilp.rs index 9487a8a2b..1217c42e3 100644 --- a/src/rules/multiprocessorscheduling_ilp.rs +++ b/src/rules/multiprocessorscheduling_ilp.rs @@ -37,16 +37,14 @@ impl ReductionResult for ReductionMSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let num_processors = self.num_processors; - (0..self.num_tasks) - .map(|j| { - (0..num_processors) - .find(|&p| target_solution[j * num_processors + p] == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.num_tasks, + self.num_processors, + 0, + ) } } diff --git a/src/rules/naesatisfiability_ilp.rs b/src/rules/naesatisfiability_ilp.rs index bed2ca447..199ba9508 100644 --- a/src/rules/naesatisfiability_ilp.rs +++ b/src/rules/naesatisfiability_ilp.rs @@ -30,6 +30,8 @@ impl ReductionResult for ReductionNAESATToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/naesatisfiability_maxcut.rs b/src/rules/naesatisfiability_maxcut.rs index eda476a4c..aad896f2f 100644 --- a/src/rules/naesatisfiability_maxcut.rs +++ b/src/rules/naesatisfiability_maxcut.rs @@ -40,6 +40,8 @@ impl ReductionResult for ReductionNAESATToMaxCut { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ (0..self.source_num_vars) .map(|i| target_solution[2 * i]) diff --git a/src/rules/naesatisfiability_partitionintoperfectmatchings.rs b/src/rules/naesatisfiability_partitionintoperfectmatchings.rs index 447f73832..346b1328d 100644 --- a/src/rules/naesatisfiability_partitionintoperfectmatchings.rs +++ b/src/rules/naesatisfiability_partitionintoperfectmatchings.rs @@ -69,6 +69,8 @@ impl ReductionResult for ReductionNAESATToPartitionIntoPerfectMatchings { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ self.layout .variables diff --git a/src/rules/naesatisfiability_setsplitting.rs b/src/rules/naesatisfiability_setsplitting.rs index 915df3614..7d8d5818c 100644 --- a/src/rules/naesatisfiability_setsplitting.rs +++ b/src/rules/naesatisfiability_setsplitting.rs @@ -29,15 +29,9 @@ impl ReductionResult for ReductionNAESATToSetSplitting { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - assert!( - target_solution.len() >= self.num_source_variables, - "SetSplitting solution has {} variables but source requires {}", - target_solution.len(), - self.num_source_variables, - ); - target_solution[..self.num_source_variables].to_vec() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + Ok(target_solution[..self.num_source_variables].to_vec()) } } diff --git a/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs b/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs index 982048245..1d505df98 100644 --- a/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs +++ b/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs @@ -30,12 +30,18 @@ impl ReductionResult for ReductionN3DMToNMTS { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let mut x_indices_by_pair_sum: BTreeMap> = BTreeMap::new(); for (x_index, &y_index) in target_solution.iter().enumerate() { let pair_sum = self.target.sizes_x()[x_index] .checked_add(self.target.sizes_y()[y_index]) - .expect("NMTS witness must not overflow i64 pair sums"); + .ok_or_else(|| { + crate::rules::ExtractionError::invalid( + "target pair sum overflows the target numeric domain", + ) + })?; x_indices_by_pair_sum .entry(pair_sum) .or_default() @@ -49,7 +55,11 @@ impl ReductionResult for ReductionN3DMToNMTS { let x_index = x_indices_by_pair_sum .get_mut(&target_sum) .and_then(Vec::pop) - .expect("satisfying NMTS witness must realize every target complement"); + .ok_or_else(|| { + crate::rules::ExtractionError::invalid(format!( + "target matching does not realize required pair sum {target_sum}" + )) + })?; x_perm.push(x_index); y_perm.push(target_solution[x_index]); } diff --git a/src/rules/numericalmatchingwithtargetsums_ilp.rs b/src/rules/numericalmatchingwithtargetsums_ilp.rs index ae04dcc03..c5b19c695 100644 --- a/src/rules/numericalmatchingwithtargetsums_ilp.rs +++ b/src/rules/numericalmatchingwithtargetsums_ilp.rs @@ -48,6 +48,8 @@ impl ReductionResult for ReductionNMTSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let mut assignment = vec![0usize; self.m]; for (var_idx, triple) in self.triples.iter().enumerate() { diff --git a/src/rules/openshopscheduling_ilp.rs b/src/rules/openshopscheduling_ilp.rs index b12c18fc9..4a8393998 100644 --- a/src/rules/openshopscheduling_ilp.rs +++ b/src/rules/openshopscheduling_ilp.rs @@ -92,6 +92,8 @@ impl ReductionResult for ReductionOSSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_jobs; let m = self.num_machines; @@ -99,7 +101,7 @@ impl ReductionResult for ReductionOSSToILP { // Read start times s_{j,i} for each (j, i) let start = |j: usize, i: usize| -> usize { let idx = self.num_order_vars + j * m + i; - target_solution.get(idx).copied().unwrap_or(0) + target_solution[idx] }; // For each machine, sort jobs by their start time on that machine diff --git a/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs b/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs index 443b1df09..834f51e56 100644 --- a/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs +++ b/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs @@ -49,6 +49,8 @@ impl ReductionResult for ReductionOptimalLinearArrangementToConsecutiveOnesMatri &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ match &self.construction { // No edges: any arrangement has total length 0 <= k, so emit the @@ -63,16 +65,10 @@ impl ReductionResult for ReductionOptimalLinearArrangementToConsecutiveOnesMatri // `position`. The OLA arrangement is `f(vertex) = position`, i.e. // the inverse permutation. let n = *num_vertices; - if target_solution.len() != n { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected a permutation of {n} columns, got {} entries", - target_solution.len() - ))); - } let mut arrangement = vec![0usize; n]; let mut seen = vec![false; n]; for (position, &vertex) in target_solution.iter().enumerate() { - if vertex >= n || seen[vertex] { + if seen[vertex] { return Err(crate::rules::ExtractionError::invalid( "target column order is not a permutation", )); diff --git a/src/rules/optimallineararrangement_ilp.rs b/src/rules/optimallineararrangement_ilp.rs index afb80feac..14d9b9fa3 100644 --- a/src/rules/optimallineararrangement_ilp.rs +++ b/src/rules/optimallineararrangement_ilp.rs @@ -38,16 +38,14 @@ impl ReductionResult for ReductionOLAToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let n = self.num_vertices; - (0..n) - .map(|v| { - (0..n) - .find(|&p| target_solution[v * n + p] == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.num_vertices, + self.num_vertices, + 0, + ) } } diff --git a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs index 96c280645..017425120 100644 --- a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs +++ b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs @@ -36,10 +36,16 @@ impl ReductionResult for ReductionOLAToSequencingToMinimizeWeightedCompletionTim &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let schedule = crate::models::misc::decode_lehmer(target_solution, self.target.num_tasks()) - .expect("target solution must be a valid Lehmer code"); + .ok_or_else(|| { + crate::rules::ExtractionError::invalid( + "target configuration is not a Lehmer code", + ) + })?; let mut arrangement = vec![0usize; self.num_vertices]; let mut next_position = 0usize; diff --git a/src/rules/optimumcommunicationspanningtree_ilp.rs b/src/rules/optimumcommunicationspanningtree_ilp.rs index 0470e0359..7f98ad847 100644 --- a/src/rules/optimumcommunicationspanningtree_ilp.rs +++ b/src/rules/optimumcommunicationspanningtree_ilp.rs @@ -37,6 +37,8 @@ impl ReductionResult for ReductionOptimumCommunicationSpanningTreeToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_edges].to_vec()) } } diff --git a/src/rules/paintshop_ilp.rs b/src/rules/paintshop_ilp.rs index 146cf6979..370e6b49e 100644 --- a/src/rules/paintshop_ilp.rs +++ b/src/rules/paintshop_ilp.rs @@ -28,6 +28,8 @@ impl ReductionResult for ReductionPaintShopToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_cars].to_vec()) } } diff --git a/src/rules/paintshop_qubo.rs b/src/rules/paintshop_qubo.rs index 9cb719e51..105bedb8f 100644 --- a/src/rules/paintshop_qubo.rs +++ b/src/rules/paintshop_qubo.rs @@ -32,6 +32,8 @@ impl ReductionResult for ReductionPaintShopToQUBO { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/partiallyorderedknapsack_ilp.rs b/src/rules/partiallyorderedknapsack_ilp.rs index 5fe35bed5..5352058c1 100644 --- a/src/rules/partiallyorderedknapsack_ilp.rs +++ b/src/rules/partiallyorderedknapsack_ilp.rs @@ -25,6 +25,8 @@ impl ReductionResult for ReductionPOKToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/partition_binpacking.rs b/src/rules/partition_binpacking.rs index 715c8e926..e070fc5d0 100644 --- a/src/rules/partition_binpacking.rs +++ b/src/rules/partition_binpacking.rs @@ -34,6 +34,8 @@ impl ReductionResult for ReductionPartitionToBinPacking { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // BinPacking may use any bin indices (0..n-1). Remap the two distinct // bins used in a 2-bin packing to Partition's {0, 1} assignment. diff --git a/src/rules/partition_cosineproductintegration.rs b/src/rules/partition_cosineproductintegration.rs index b5c262481..b449cb8b7 100644 --- a/src/rules/partition_cosineproductintegration.rs +++ b/src/rules/partition_cosineproductintegration.rs @@ -32,6 +32,8 @@ impl ReductionResult for ReductionPartitionToCPI { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/partition_integralflowwithmultipliers.rs b/src/rules/partition_integralflowwithmultipliers.rs index 39cf06143..ac590f3e7 100644 --- a/src/rules/partition_integralflowwithmultipliers.rs +++ b/src/rules/partition_integralflowwithmultipliers.rs @@ -36,13 +36,7 @@ impl ReductionResult for ReductionPartitionToIntegralFlowWithMultipliers { "the fixed infeasible target instance has no extractable witness", ) })?; - if target_solution.len() < item_arc_count { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected at least {} flow values, got {}", - item_arc_count, - target_solution.len() - ))); - } + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; target_solution[..item_arc_count].to_vec() }) diff --git a/src/rules/partition_knapsack.rs b/src/rules/partition_knapsack.rs index 9bddbef27..d2539f60f 100644 --- a/src/rules/partition_knapsack.rs +++ b/src/rules/partition_knapsack.rs @@ -22,6 +22,8 @@ impl ReductionResult for ReductionPartitionToKnapsack { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/partition_multiprocessorscheduling.rs b/src/rules/partition_multiprocessorscheduling.rs index f1e54a355..0793a191e 100644 --- a/src/rules/partition_multiprocessorscheduling.rs +++ b/src/rules/partition_multiprocessorscheduling.rs @@ -36,6 +36,8 @@ impl ReductionResult for ReductionPartitionToMPS { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/partition_openshopscheduling.rs b/src/rules/partition_openshopscheduling.rs index 6bd8a5193..68bb46050 100644 --- a/src/rules/partition_openshopscheduling.rs +++ b/src/rules/partition_openshopscheduling.rs @@ -21,8 +21,10 @@ impl ReductionResult for ReductionPartitionToOpenShopScheduling { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ - let num_elements = self.target.num_jobs().saturating_sub(1); + let num_elements = self.target.num_jobs() - 1; let mut source_config = vec![0; num_elements]; let Some(orders) = self.target.decode_orders(target_solution) else { return Err(crate::rules::ExtractionError::invalid( @@ -60,9 +62,17 @@ impl ReductionResult for ReductionPartitionToOpenShopScheduling { } } } - let (start, mi, job) = best.expect("schedule incomplete"); + let (start, mi, job) = best.ok_or_else(|| { + crate::rules::ExtractionError::invalid("target schedule is incomplete") + })?; start_times[job][mi] = start; - let end = start + self.target.processing_times()[job][mi]; + let end = start + .checked_add(self.target.processing_times()[job][mi]) + .ok_or_else(|| { + crate::rules::ExtractionError::invalid( + "target schedule time overflows usize", + ) + })?; machine_avail[mi] = end; job_avail[job] = end; cursor[mi] += 1; @@ -71,16 +81,21 @@ impl ReductionResult for ReductionPartitionToOpenShopScheduling { // Find the middle machine where the special job starts at half_sum let middle_machine = (0..m) .find(|&machine| start_times[special_job][machine] == half_sum) - .unwrap_or_else(|| { - let mut machines: Vec = (0..m).collect(); - machines.sort_by_key(|&machine| (start_times[special_job][machine], machine)); - machines[m / 2] - }); + .ok_or_else(|| { + crate::rules::ExtractionError::invalid( + "target schedule has no machine at the partition boundary", + ) + })?; let pivot = start_times[special_job][middle_machine]; for (job, slot) in source_config.iter_mut().enumerate() { let completion = start_times[job][middle_machine] - + self.target.processing_times()[job][middle_machine]; + .checked_add(self.target.processing_times()[job][middle_machine]) + .ok_or_else(|| { + crate::rules::ExtractionError::invalid( + "target schedule time overflows usize", + ) + })?; if completion <= pivot { *slot = 1; } diff --git a/src/rules/partition_productionplanning.rs b/src/rules/partition_productionplanning.rs index 6a1f0c6fd..b18798007 100644 --- a/src/rules/partition_productionplanning.rs +++ b/src/rules/partition_productionplanning.rs @@ -21,13 +21,12 @@ impl ReductionResult for ReductionPartitionToProductionPlanning { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - target_solution - .iter() - .take(self.target.num_periods().saturating_sub(1)) - .map(|&production| usize::from(production > 0)) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + Ok(target_solution[..self.target.num_periods() - 1] + .iter() + .map(|&production| usize::from(production > 0)) + .collect()) } } diff --git a/src/rules/partition_sequencingtominimizetardytaskweight.rs b/src/rules/partition_sequencingtominimizetardytaskweight.rs index 05c6409e6..9c4259ab5 100644 --- a/src/rules/partition_sequencingtominimizetardytaskweight.rs +++ b/src/rules/partition_sequencingtominimizetardytaskweight.rs @@ -10,21 +10,6 @@ pub struct ReductionPartitionToSequencingToMinimizeTardyTaskWeight { target: SequencingToMinimizeTardyTaskWeight, } -impl ReductionPartitionToSequencingToMinimizeTardyTaskWeight { - fn decode_schedule(&self, target_solution: &[usize]) -> Vec { - let n = self.target.num_tasks(); - assert_eq!( - target_solution.len(), - n, - "target solution length must equal target num_tasks" - ); - - // The target model uses direct permutation encoding (dims = [n; n]). - // Each position is a task index; the solver returns a valid permutation. - target_solution.to_vec() - } -} - impl ReductionResult for ReductionPartitionToSequencingToMinimizeTardyTaskWeight { type Source = Partition; type Target = SequencingToMinimizeTardyTaskWeight; @@ -37,15 +22,29 @@ impl ReductionResult for ReductionPartitionToSequencingToMinimizeTardyTaskWeight &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ - let schedule = self.decode_schedule(target_solution); + let mut seen = vec![false; self.target.num_tasks()]; + for &task in target_solution { + if std::mem::replace(&mut seen[task], true) { + return Err(crate::rules::ExtractionError::invalid(format!( + "target schedule contains task {task} more than once" + ))); + } + } + let mut source_config = vec![1; self.target.num_tasks()]; let mut completion_time = 0u64; - for task in schedule { + for &task in target_solution { completion_time = completion_time .checked_add(self.target.lengths()[task]) - .expect("completion time overflowed u64"); + .ok_or_else(|| { + crate::rules::ExtractionError::invalid( + "target schedule completion time overflows u64", + ) + })?; if completion_time <= self.target.deadlines()[task] { source_config[task] = 0; } diff --git a/src/rules/partition_subsetsum.rs b/src/rules/partition_subsetsum.rs index 26526461a..3c6011ced 100644 --- a/src/rules/partition_subsetsum.rs +++ b/src/rules/partition_subsetsum.rs @@ -30,6 +30,8 @@ impl ReductionResult for ReductionPartitionToSubsetSum { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + if target_solution.len() != self.source_n { return Err(crate::rules::ExtractionError::invalid(format!( "expected {} subset-selection values, got {}", diff --git a/src/rules/partition_sumofsquarespartition.rs b/src/rules/partition_sumofsquarespartition.rs index 1d6b642ce..e095626ee 100644 --- a/src/rules/partition_sumofsquarespartition.rs +++ b/src/rules/partition_sumofsquarespartition.rs @@ -49,13 +49,8 @@ impl ReductionResult for ReductionPartitionToSumOfSquaresPartition { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - let expected = self.target.num_elements(); - if target_solution.len() != expected { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {expected} group assignments, got {}", - target_solution.len() - ))); - } + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.source_n].to_vec()) } } diff --git a/src/rules/partitionintocliques_minimumcoveringbycliques.rs b/src/rules/partitionintocliques_minimumcoveringbycliques.rs index 6b4367928..2c13d29cc 100644 --- a/src/rules/partitionintocliques_minimumcoveringbycliques.rs +++ b/src/rules/partitionintocliques_minimumcoveringbycliques.rs @@ -108,17 +108,11 @@ impl ReductionResult for ReductionPartitionIntoCliquesToMinimumCoveringByCliques &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.source_graph.num_vertices(); let target_edges = self.target.graph().edges(); - if target_solution.len() != target_edges.len() { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} edge labels, got {}", - target_edges.len(), - target_solution.len() - ))); - } - let mut matching_labels = vec![None; n]; for ((u, v), &label) in target_edges.iter().zip(target_solution.iter()) { let matching_index = if *u < n && *v == n + *u { @@ -134,21 +128,19 @@ impl ReductionResult for ReductionPartitionIntoCliquesToMinimumCoveringByCliques } } - if matching_labels.iter().any(Option::is_none) { - return Err(crate::rules::ExtractionError::invalid( - "target cover does not label every matching gadget edge", - )); - } - let mut label_map = BTreeMap::new(); let extracted = matching_labels .into_iter() .map(|label| { - let label = label.expect("checked above"); + let label = label.ok_or_else(|| { + crate::rules::ExtractionError::invalid( + "target cover does not label every matching gadget edge", + ) + })?; let next = label_map.len(); - *label_map.entry(label).or_insert(next) + Ok(*label_map.entry(label).or_insert(next)) }) - .collect::>(); + .collect::>>()?; if label_map.len() > self.source_num_cliques { return Err(crate::rules::ExtractionError::invalid(format!( diff --git a/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs b/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs index 7e856e100..bb8d149c3 100644 --- a/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs +++ b/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs @@ -37,6 +37,8 @@ impl ReductionResult for ReductionPPL2ToBCSF { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/partitionintopathsoflength2_ilp.rs b/src/rules/partitionintopathsoflength2_ilp.rs index 1c5540d36..fcaee2e58 100644 --- a/src/rules/partitionintopathsoflength2_ilp.rs +++ b/src/rules/partitionintopathsoflength2_ilp.rs @@ -47,19 +47,14 @@ impl ReductionResult for ReductionPIPL2ToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let num_groups = self.num_groups; - (0..self.num_vertices) - .map(|v| { - (0..num_groups) - .find(|&g| { - let idx = v * num_groups + g; - idx < target_solution.len() && target_solution[idx] == 1 - }) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.num_vertices, + self.num_groups, + 0, + ) } } diff --git a/src/rules/partitionintotriangles_ilp.rs b/src/rules/partitionintotriangles_ilp.rs index dc83de3bc..cb31412f1 100644 --- a/src/rules/partitionintotriangles_ilp.rs +++ b/src/rules/partitionintotriangles_ilp.rs @@ -41,19 +41,14 @@ impl ReductionResult for ReductionPITToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let num_groups = self.num_groups; - (0..self.num_vertices) - .map(|v| { - (0..num_groups) - .find(|&g| { - let idx = v * num_groups + g; - idx < target_solution.len() && target_solution[idx] == 1 - }) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.num_vertices, + self.num_groups, + 0, + ) } } diff --git a/src/rules/pathconstrainednetworkflow_ilp.rs b/src/rules/pathconstrainednetworkflow_ilp.rs index 30f787a49..aab353b21 100644 --- a/src/rules/pathconstrainednetworkflow_ilp.rs +++ b/src/rules/pathconstrainednetworkflow_ilp.rs @@ -26,6 +26,8 @@ impl ReductionResult for ReductionPCNFToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/precedenceconstrainedscheduling_ilp.rs b/src/rules/precedenceconstrainedscheduling_ilp.rs index 86fcb73d5..351c37021 100644 --- a/src/rules/precedenceconstrainedscheduling_ilp.rs +++ b/src/rules/precedenceconstrainedscheduling_ilp.rs @@ -42,16 +42,14 @@ impl ReductionResult for ReductionPCSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let d = self.deadline; - (0..self.num_tasks) - .map(|j| { - (0..d) - .find(|&t| target_solution.get(j * d + t).copied().unwrap_or(0) == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.num_tasks, + self.deadline, + 0, + ) } } diff --git a/src/rules/preemptivescheduling_ilp.rs b/src/rules/preemptivescheduling_ilp.rs index 37a55560d..b5a2b6203 100644 --- a/src/rules/preemptivescheduling_ilp.rs +++ b/src/rules/preemptivescheduling_ilp.rs @@ -55,9 +55,11 @@ impl ReductionResult for ReductionPSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let nd = self.num_tasks * self.d_max; - target_solution[..nd.min(target_solution.len())].to_vec() + target_solution[..nd].to_vec() }) } } diff --git a/src/rules/prizecollectingsteinerforest_steinertree.rs b/src/rules/prizecollectingsteinerforest_steinertree.rs index fed6438a9..67c05cda4 100644 --- a/src/rules/prizecollectingsteinerforest_steinertree.rs +++ b/src/rules/prizecollectingsteinerforest_steinertree.rs @@ -73,6 +73,8 @@ impl ReductionResult for ReductionPCSFToSteinerTree { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_source_vertices; let m = self.num_source_edges; @@ -97,7 +99,7 @@ impl ReductionResult for ReductionPCSFToSteinerTree { // (this also covers prize-zero endpoints, which have no gadget). let edges = self.target.graph().edges(); for (target_idx, &(_, _)) in edges.iter().enumerate() { - if target_solution.get(target_idx).copied() != Some(1) { + if target_solution[target_idx] != 1 { continue; } if let Some(src_edge) = self.target_to_source_edge[target_idx] { diff --git a/src/rules/quadraticassignment_ilp.rs b/src/rules/quadraticassignment_ilp.rs index fc5f9bfcc..2e8736902 100644 --- a/src/rules/quadraticassignment_ilp.rs +++ b/src/rules/quadraticassignment_ilp.rs @@ -38,16 +38,14 @@ impl ReductionResult for ReductionQAPToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let loc = self.num_locations; - (0..self.num_facilities) - .map(|i| { - (0..loc) - .find(|&p| target_solution[i * loc + p] == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.num_facilities, + self.num_locations, + 0, + ) } } diff --git a/src/rules/qubo_ilp.rs b/src/rules/qubo_ilp.rs index 75b1e7792..799d15388 100644 --- a/src/rules/qubo_ilp.rs +++ b/src/rules/qubo_ilp.rs @@ -37,6 +37,8 @@ impl ReductionResult for ReductionQUBOToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_original].to_vec()) } } diff --git a/src/rules/rectilinearpicturecompression_ilp.rs b/src/rules/rectilinearpicturecompression_ilp.rs index 94ff40d3c..934fd4edc 100644 --- a/src/rules/rectilinearpicturecompression_ilp.rs +++ b/src/rules/rectilinearpicturecompression_ilp.rs @@ -25,6 +25,8 @@ impl ReductionResult for ReductionRPCToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/registersufficiency_ilp.rs b/src/rules/registersufficiency_ilp.rs index 788c3cba6..ed6615d47 100644 --- a/src/rules/registersufficiency_ilp.rs +++ b/src/rules/registersufficiency_ilp.rs @@ -30,6 +30,8 @@ impl ReductionResult for ReductionRegisterSufficiencyToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_vertices].to_vec()) } } diff --git a/src/rules/resourceconstrainedscheduling_ilp.rs b/src/rules/resourceconstrainedscheduling_ilp.rs index 2301d357b..e525d1b9e 100644 --- a/src/rules/resourceconstrainedscheduling_ilp.rs +++ b/src/rules/resourceconstrainedscheduling_ilp.rs @@ -33,16 +33,14 @@ impl ReductionResult for ReductionRCSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let d = self.deadline; - (0..self.num_tasks) - .map(|j| { - (0..d) - .find(|&t| target_solution.get(j * d + t).copied().unwrap_or(0) == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.num_tasks, + self.deadline, + 0, + ) } } diff --git a/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs b/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs index 9ec80e2f4..779527a6f 100644 --- a/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs +++ b/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs @@ -40,6 +40,8 @@ impl ReductionResult for ReductionRootedTreeArrangementToRootedTreeStorageAssign &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_vertices; // target_solution is the parent array of the rooted tree on X = V diff --git a/src/rules/rootedtreestorageassignment_ilp.rs b/src/rules/rootedtreestorageassignment_ilp.rs index d60fc2c65..6019fdd8b 100644 --- a/src/rules/rootedtreestorageassignment_ilp.rs +++ b/src/rules/rootedtreestorageassignment_ilp.rs @@ -7,6 +7,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::RootedTreeStorageAssignment; use crate::reduction; +use crate::rules::ilp_helpers::one_hot_decode_rows; use crate::rules::traits::{ReduceTo, ReductionResult}; // Index helpers @@ -75,16 +76,9 @@ impl ReductionResult for ReductionRTSAToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let n = self.n; - (0..n) - .map(|v| { - (0..n) - .find(|&u| target_solution[idx_p(n, v, u)] == 1) - .unwrap_or(v) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + one_hot_decode_rows(target_solution, self.n, self.n, 0) } } diff --git a/src/rules/ruralpostman_ilp.rs b/src/rules/ruralpostman_ilp.rs index 01785f301..e892f67f1 100644 --- a/src/rules/ruralpostman_ilp.rs +++ b/src/rules/ruralpostman_ilp.rs @@ -30,6 +30,8 @@ impl ReductionResult for ReductionRPToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // Output the traversal multiplicities t_e target_solution[..self.num_edges].to_vec() diff --git a/src/rules/sat_circuitsat.rs b/src/rules/sat_circuitsat.rs index a2236d72c..d3ae3a7d3 100644 --- a/src/rules/sat_circuitsat.rs +++ b/src/rules/sat_circuitsat.rs @@ -30,6 +30,8 @@ impl ReductionResult for ReductionSATToCircuit { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ self.source_var_indices .iter() diff --git a/src/rules/sat_coloring.rs b/src/rules/sat_coloring.rs index be2273643..47bb3d305 100644 --- a/src/rules/sat_coloring.rs +++ b/src/rules/sat_coloring.rs @@ -244,22 +244,20 @@ impl ReductionResult for ReductionSATToColoring { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // First determine which color is TRUE, FALSE, and AUX // Vertices 0, 1, 2 are TRUE, FALSE, AUX respectively - assert!( - target_solution.len() >= 3, - "Invalid solution: coloring must have at least 3 vertices" - ); let true_color = target_solution[0]; let false_color = target_solution[1]; let aux_color = target_solution[2]; - // Sanity checks - assert!( - true_color != false_color && true_color != aux_color, - "Invalid coloring solution: special vertices must have distinct colors" - ); + if true_color == false_color || true_color == aux_color || false_color == aux_color { + return Err(crate::rules::ExtractionError::invalid( + "target coloring does not distinguish true, false, and auxiliary colors", + )); + } let mut assignment = vec![0usize; self.num_source_variables]; @@ -267,10 +265,11 @@ impl ReductionResult for ReductionSATToColoring { let vertex_color = target_solution[pos_vertex]; // Sanity check: variable vertices should not have AUX color - assert!( - vertex_color != aux_color, - "Invalid coloring solution: variable vertex has auxiliary color" - ); + if vertex_color == aux_color { + return Err(crate::rules::ExtractionError::invalid(format!( + "variable {i} has the auxiliary color" + ))); + } // If positive literal has TRUE color, variable is true (1) // Otherwise, variable is false (0) diff --git a/src/rules/sat_ksat.rs b/src/rules/sat_ksat.rs index ea73fa1a2..2bf711699 100644 --- a/src/rules/sat_ksat.rs +++ b/src/rules/sat_ksat.rs @@ -35,6 +35,8 @@ impl ReductionResult for ReductionSATToKSAT { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // Only return the original variables, discarding ancillas target_solution[..self.source_num_vars].to_vec() @@ -171,6 +173,8 @@ impl ReductionResult for ReductionKSATToSAT { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // Direct mapping - no transformation needed target_solution.to_vec() diff --git a/src/rules/sat_maximumindependentset.rs b/src/rules/sat_maximumindependentset.rs index 6d8ba24e3..09cdc61c0 100644 --- a/src/rules/sat_maximumindependentset.rs +++ b/src/rules/sat_maximumindependentset.rs @@ -80,6 +80,8 @@ impl ReductionResult for ReductionSATToIS { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let mut assignment = vec![0usize; self.num_source_variables]; let mut covered = vec![false; self.num_source_variables]; diff --git a/src/rules/sat_minimumdominatingset.rs b/src/rules/sat_minimumdominatingset.rs index dd3bccbf6..3f78049e9 100644 --- a/src/rules/sat_minimumdominatingset.rs +++ b/src/rules/sat_minimumdominatingset.rs @@ -58,50 +58,31 @@ impl ReductionResult for ReductionSATToDS { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let selected_count: usize = target_solution.iter().sum(); - - // If more vertices selected than variables, not a minimal dominating set - // corresponding to a satisfying assignment - if selected_count > self.num_literals { - return Err(crate::rules::ExtractionError::invalid(format!( - "selected {selected_count} dominating-set vertices for {} source variables", - self.num_literals - ))); - } - - let mut assignment = vec![0usize; self.num_literals]; - - for (i, &value) in target_solution.iter().enumerate() { - if value == 1 { - // Only consider variable gadget vertices (first 3*num_literals vertices) - if i >= 3 * self.num_literals { - continue; // Skip clause vertices - } - - let var_index = i / 3; - let vertex_type = i % 3; - - match vertex_type { - 0 => { - // Positive literal selected: x_i = true - assignment[var_index] = 1; - } - 1 => { - // Negative literal selected: x_i = false - assignment[var_index] = 0; - } - 2 => { - // Dummy vertex selected: variable is unconstrained - // Default to false (already 0), but could be anything - } - _ => unreachable!(), - } - } - } + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + let assignment = target_solution[..3 * self.num_literals] + .chunks_exact(3) + .enumerate() + .map(|(variable, gadget)| match gadget { + [1, 0, 0] => Ok(1), + [0, 1, 0] | [0, 0, 1] => Ok(0), + _ => Err(crate::rules::ExtractionError::invalid(format!( + "variable {variable} gadget must select exactly one vertex, got {}", + gadget.iter().sum::() + ))), + }) + .collect::>>()?; + + if let Some(clause) = target_solution[3 * self.num_literals..] + .iter() + .position(|&selected| selected == 1) + { + return Err(crate::rules::ExtractionError::invalid(format!( + "clause vertex {clause} is selected" + ))); + } - assignment - }) + Ok(assignment) } } diff --git a/src/rules/satisfiability_integralflowhomologousarcs.rs b/src/rules/satisfiability_integralflowhomologousarcs.rs index 227a1c387..ec7e9ee87 100644 --- a/src/rules/satisfiability_integralflowhomologousarcs.rs +++ b/src/rules/satisfiability_integralflowhomologousarcs.rs @@ -106,18 +106,12 @@ impl ReductionResult for ReductionSATToIntegralFlowHomologousArcs { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ self.variable_paths .iter() - .map(|paths| { - usize::from( - target_solution - .get(paths.true_base_arc) - .copied() - .unwrap_or(0) - > 0, - ) - }) + .map(|paths| usize::from(target_solution[paths.true_base_arc] > 0)) .collect() }) } diff --git a/src/rules/satisfiability_maximum2satisfiability.rs b/src/rules/satisfiability_maximum2satisfiability.rs index d959c8e1b..8b375f917 100644 --- a/src/rules/satisfiability_maximum2satisfiability.rs +++ b/src/rules/satisfiability_maximum2satisfiability.rs @@ -23,6 +23,8 @@ impl ReductionResult for ReductionSatisfiabilityToMaximum2Satisfiability { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.source_num_vars].to_vec()) } } diff --git a/src/rules/satisfiability_naesatisfiability.rs b/src/rules/satisfiability_naesatisfiability.rs index c0073d313..0f90f23bb 100644 --- a/src/rules/satisfiability_naesatisfiability.rs +++ b/src/rules/satisfiability_naesatisfiability.rs @@ -32,25 +32,9 @@ impl ReductionResult for ReductionSATToNAESAT { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - let n = self.source_num_vars; - let expected = n + 1; - if target_solution.len() != expected { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {expected} values including the sentinel, got {}", - target_solution.len() - ))); - } - if let Some((index, value)) = target_solution - .iter() - .copied() - .enumerate() - .find(|(_, value)| *value > 1) - { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected a binary value at position {index}, got {value}" - ))); - } + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + let n = self.source_num_vars; let sentinel = target_solution[n]; Ok(target_solution[..n] .iter() diff --git a/src/rules/satisfiability_nontautology.rs b/src/rules/satisfiability_nontautology.rs index 385891290..696c1896b 100644 --- a/src/rules/satisfiability_nontautology.rs +++ b/src/rules/satisfiability_nontautology.rs @@ -25,6 +25,8 @@ impl ReductionResult for ReductionSATToNonTautology { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs index 8ad396270..379fece7a 100644 --- a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs @@ -8,6 +8,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SchedulingToMinimizeWeightedCompletionTime; use crate::reduction; +use crate::rules::ilp_helpers::one_hot_decode_rows; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing SchedulingToMinimizeWeightedCompletionTime to ILP. @@ -55,15 +56,9 @@ impl ReductionResult for ReductionSMWCTToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - (0..self.num_tasks) - .map(|t| { - (0..self.num_processors) - .find(|&p| target_solution[self.x_var(t, p)] == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + one_hot_decode_rows(target_solution, self.num_tasks, self.num_processors, 0) } } diff --git a/src/rules/schedulingwithindividualdeadlines_ilp.rs b/src/rules/schedulingwithindividualdeadlines_ilp.rs index 850c52348..3d3582039 100644 --- a/src/rules/schedulingwithindividualdeadlines_ilp.rs +++ b/src/rules/schedulingwithindividualdeadlines_ilp.rs @@ -14,6 +14,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SchedulingWithIndividualDeadlines; use crate::reduction; +use crate::rules::ilp_helpers::one_hot_decode_rows; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing SchedulingWithIndividualDeadlines to ILP. @@ -42,16 +43,9 @@ impl ReductionResult for ReductionSWIDToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let d = self.max_deadline; - (0..self.num_tasks) - .map(|j| { - (0..d) - .find(|&t| target_solution.get(j * d + t).copied().unwrap_or(0) == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + one_hot_decode_rows(target_solution, self.num_tasks, self.max_deadline, 0) } } diff --git a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs index d6545b30a..6e788ad2e 100644 --- a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs +++ b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs @@ -35,9 +35,11 @@ impl ReductionResult for ReductionSTMMCCToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_tasks; - let schedule = one_hot_decode(target_solution, n, n, 0); + let schedule = one_hot_decode(target_solution, n, n, 0)?; permutation_to_lehmer(&schedule) }) } diff --git a/src/rules/sequencingtominimizetardytaskweight_ilp.rs b/src/rules/sequencingtominimizetardytaskweight_ilp.rs index 5c4a88110..2648134ef 100644 --- a/src/rules/sequencingtominimizetardytaskweight_ilp.rs +++ b/src/rules/sequencingtominimizetardytaskweight_ilp.rs @@ -29,12 +29,14 @@ impl ReductionResult for ReductionSTMTTWToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_tasks; // Decode the n*n block of x_{j,p} variables into a schedule permutation. // The source uses direct permutation encoding (config = schedule directly), // so return the schedule as-is (it is already a permutation of 0..n). - one_hot_decode(target_solution, n, n, 0) + one_hot_decode(target_solution, n, n, 0)? }) } } diff --git a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index 655154fe2..134e66b49 100644 --- a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -55,9 +55,11 @@ impl ReductionResult for ReductionSTMWCTToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let mut schedule: Vec = (0..self.num_tasks).collect(); - schedule.sort_by_key(|&task| (target_solution.get(task).copied().unwrap_or(0), task)); + schedule.sort_by_key(|&task| (target_solution[task], task)); Self::encode_schedule_as_lehmer(&schedule) }) } diff --git a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs index e8e5bb1ee..747c7846c 100644 --- a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -53,11 +53,13 @@ impl ReductionResult for ReductionSTMWTToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_tasks; let c_offset = self.num_order_vars; let mut jobs: Vec = (0..n).collect(); - jobs.sort_by_key(|&j| (target_solution.get(c_offset + j).copied().unwrap_or(0), j)); + jobs.sort_by_key(|&j| (target_solution[c_offset + j], j)); Self::encode_schedule_as_lehmer(&jobs) }) } diff --git a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs index 5c63a7e61..9af0f5db9 100644 --- a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs +++ b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs @@ -40,10 +40,12 @@ impl ReductionResult for ReductionSWDSTToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_tasks; // x_{j,p} occupies the first n*n variables: decode the permutation. - one_hot_decode(target_solution, n, n, 0) + one_hot_decode(target_solution, n, n, 0)? }) } } diff --git a/src/rules/sequencingwithinintervals_ilp.rs b/src/rules/sequencingwithinintervals_ilp.rs index 8457f2444..8424562ab 100644 --- a/src/rules/sequencingwithinintervals_ilp.rs +++ b/src/rules/sequencingwithinintervals_ilp.rs @@ -47,16 +47,24 @@ impl ReductionResult for ReductionSWIToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - self.task_layout - .iter() - .map(|&(base, count)| { - (0..count) - .find(|&k| target_solution.get(base + k).copied().unwrap_or(0) == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + self.task_layout + .iter() + .enumerate() + .map(|(task, &(base, count))| { + let mut selected = (0..count).filter(|&offset| target_solution[base + offset] == 1); + match (selected.next(), selected.next()) { + (Some(offset), None) => Ok(offset), + (None, _) => Err(crate::rules::ExtractionError::invalid(format!( + "task {task} has no selected start time" + ))), + (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( + "task {task} has multiple selected start times" + ))), + } + }) + .collect() } } diff --git a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs index 3dfca7126..21013774e 100644 --- a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs +++ b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs @@ -50,18 +50,15 @@ impl ReductionResult for ReductionSWRTDToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_tasks; let horizon = self.time_horizon; // For each task, find the start time - let mut start_times: Vec<(usize, usize)> = (0..n) - .map(|j| { - let start = (0..horizon) - .find(|&t| target_solution.get(j * horizon + t).copied().unwrap_or(0) == 1) - .unwrap_or(0); - (j, start) - }) - .collect(); + let starts = + crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, n, horizon, 0)?; + let mut start_times: Vec<_> = starts.into_iter().enumerate().collect(); // Sort by start time (break ties by task index) start_times.sort_by_key(|&(j, t)| (t, j)); let schedule: Vec = start_times.iter().map(|&(j, _)| j).collect(); diff --git a/src/rules/setsplitting_betweenness.rs b/src/rules/setsplitting_betweenness.rs index 280e6acc6..a64fee966 100644 --- a/src/rules/setsplitting_betweenness.rs +++ b/src/rules/setsplitting_betweenness.rs @@ -32,26 +32,13 @@ impl ReductionResult for ReductionSetSplittingToBetweenness { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - assert!( - target_solution.len() > self.pole, - "Betweenness solution has {} positions but pole index is {}", - target_solution.len(), - self.pole - ); - assert!( - target_solution.len() >= self.source_universe_size, - "Betweenness solution has {} positions but source requires {} elements", - target_solution.len(), - self.source_universe_size - ); + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let pole_position = target_solution[self.pole]; - target_solution[..self.source_universe_size] - .iter() - .map(|&position| usize::from(position > pole_position)) - .collect() - }) + let pole_position = target_solution[self.pole]; + Ok(target_solution[..self.source_universe_size] + .iter() + .map(|&position| usize::from(position > pole_position)) + .collect()) } } diff --git a/src/rules/setsplitting_ilp.rs b/src/rules/setsplitting_ilp.rs index 67c737081..b7191f939 100644 --- a/src/rules/setsplitting_ilp.rs +++ b/src/rules/setsplitting_ilp.rs @@ -32,6 +32,8 @@ impl ReductionResult for ReductionSetSplittingToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/shortestcommonsupersequence_ilp.rs b/src/rules/shortestcommonsupersequence_ilp.rs index fe002c0b1..2a284afd3 100644 --- a/src/rules/shortestcommonsupersequence_ilp.rs +++ b/src/rules/shortestcommonsupersequence_ilp.rs @@ -31,17 +31,14 @@ impl ReductionResult for ReductionSCSToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let b = self.max_length; - let k = self.alphabet_size + 1; // includes padding symbol - (0..b) - .map(|p| { - (0..k) - .find(|&a| target_solution[p * k + a] == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.max_length, + self.alphabet_size + 1, + 0, + ) } } diff --git a/src/rules/shortestweightconstrainedpath_ilp.rs b/src/rules/shortestweightconstrainedpath_ilp.rs index a45fea85e..d43b7bc7f 100644 --- a/src/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/rules/shortestweightconstrainedpath_ilp.rs @@ -44,20 +44,14 @@ impl ReductionResult for ReductionSWCPToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ (0..self.num_edges) .map(|edge_idx| { usize::from( - target_solution - .get(Self::arc_var(edge_idx, 0)) - .copied() - .unwrap_or(0) - > 0 - || target_solution - .get(Self::arc_var(edge_idx, 1)) - .copied() - .unwrap_or(0) - > 0, + target_solution[Self::arc_var(edge_idx, 0)] > 0 + || target_solution[Self::arc_var(edge_idx, 1)] > 0, ) }) .collect() diff --git a/src/rules/sparsematrixcompression_ilp.rs b/src/rules/sparsematrixcompression_ilp.rs index 209378a1d..a406f0580 100644 --- a/src/rules/sparsematrixcompression_ilp.rs +++ b/src/rules/sparsematrixcompression_ilp.rs @@ -26,16 +26,14 @@ impl ReductionResult for ReductionSMCToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - // For each row r, output the unique zero-based shift g with x_{r,g} = 1 - (0..self.num_rows) - .map(|r| { - (0..self.bound_k) - .find(|&g| target_solution[r * self.bound_k + g] == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.num_rows, + self.bound_k, + 0, + ) } } diff --git a/src/rules/spinglass_maxcut.rs b/src/rules/spinglass_maxcut.rs index c237cf4cc..ac6e3a610 100644 --- a/src/rules/spinglass_maxcut.rs +++ b/src/rules/spinglass_maxcut.rs @@ -40,6 +40,8 @@ where &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } @@ -119,6 +121,8 @@ where &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ match self.ancilla { None => target_solution.to_vec(), diff --git a/src/rules/spinglass_qubo.rs b/src/rules/spinglass_qubo.rs index bf29ea5c0..f77b3353b 100644 --- a/src/rules/spinglass_qubo.rs +++ b/src/rules/spinglass_qubo.rs @@ -30,6 +30,8 @@ impl ReductionResult for ReductionQUBOToSG { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } @@ -108,6 +110,8 @@ impl ReductionResult for ReductionSGToQUBO { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/stackercrane_ilp.rs b/src/rules/stackercrane_ilp.rs index 3937557fb..7277f6bc6 100644 --- a/src/rules/stackercrane_ilp.rs +++ b/src/rules/stackercrane_ilp.rs @@ -35,9 +35,11 @@ impl ReductionResult for ReductionSCToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // Decode the permutation: for each position p, find the arc a with x_{a,p} = 1 - one_hot_decode(target_solution, self.num_arcs, self.num_arcs, 0) + one_hot_decode(target_solution, self.num_arcs, self.num_arcs, 0)? }) } } diff --git a/src/rules/steinertree_ilp.rs b/src/rules/steinertree_ilp.rs index 496be693a..c6ab0162d 100644 --- a/src/rules/steinertree_ilp.rs +++ b/src/rules/steinertree_ilp.rs @@ -37,6 +37,8 @@ impl ReductionResult for ReductionSteinerTreeToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_edges].to_vec()) } } diff --git a/src/rules/steinertreeingraphs_ilp.rs b/src/rules/steinertreeingraphs_ilp.rs index 67219a73a..1404ea117 100644 --- a/src/rules/steinertreeingraphs_ilp.rs +++ b/src/rules/steinertreeingraphs_ilp.rs @@ -37,6 +37,8 @@ impl ReductionResult for ReductionSTIGToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_edges].to_vec()) } } diff --git a/src/rules/stringtostringcorrection_ilp.rs b/src/rules/stringtostringcorrection_ilp.rs index a476702fb..ab0ca3b6d 100644 --- a/src/rules/stringtostringcorrection_ilp.rs +++ b/src/rules/stringtostringcorrection_ilp.rs @@ -58,6 +58,8 @@ impl ReductionResult for ReductionSTSCToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.n; let k = self.bound; @@ -76,28 +78,27 @@ impl ReductionResult for ReductionSTSCToILP { .filter(|&p| target_solution[idx_e(n, k, t - 1, p)] == 0) .count(); + let mut selected = Vec::new(); if target_solution[idx_nu(n, k, t)] == 1 { - ops.push(noop_code); - } else { - let mut found = false; - for j in 0..n { - if target_solution[idx_d(n, k, t, j)] == 1 { - ops.push(j); - found = true; - break; - } + selected.push(noop_code); + } + selected.extend((0..n).filter(|&j| target_solution[idx_d(n, k, t, j)] == 1)); + selected.extend( + (0..nm1) + .filter(|&j| target_solution[idx_s(n, k, t, j)] == 1) + .map(|j| current_len + j), + ); + match selected.as_slice() { + [operation] => ops.push(*operation), + [] => { + return Err(crate::rules::ExtractionError::invalid(format!( + "edit step {t} has no selected operation" + ))) } - if !found { - for j in 0..nm1 { - if target_solution[idx_s(n, k, t, j)] == 1 { - ops.push(current_len + j); - found = true; - break; - } - } - if !found { - ops.push(noop_code); - } + _ => { + return Err(crate::rules::ExtractionError::invalid(format!( + "edit step {t} has multiple selected operations" + ))) } } } diff --git a/src/rules/strongconnectivityaugmentation_ilp.rs b/src/rules/strongconnectivityaugmentation_ilp.rs index 81727c373..66638ae19 100644 --- a/src/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/rules/strongconnectivityaugmentation_ilp.rs @@ -27,6 +27,8 @@ impl ReductionResult for ReductionSCAToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.num_candidates].to_vec()) } } diff --git a/src/rules/subgraphisomorphism_ilp.rs b/src/rules/subgraphisomorphism_ilp.rs index 5839bae85..d4241e263 100644 --- a/src/rules/subgraphisomorphism_ilp.rs +++ b/src/rules/subgraphisomorphism_ilp.rs @@ -10,7 +10,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::SubgraphIsomorphism; use crate::reduction; -use crate::rules::ilp_helpers::one_hot_assignment_constraints; +use crate::rules::ilp_helpers::{one_hot_assignment_constraints, one_hot_decode_rows}; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::Graph; @@ -38,16 +38,14 @@ impl ReductionResult for ReductionSubIsoToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let n_host = self.num_host_vertices; - (0..self.num_pattern_vertices) - .map(|v| { - (0..n_host) - .find(|&u| target_solution[v * n_host + u] == 1) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + one_hot_decode_rows( + target_solution, + self.num_pattern_vertices, + self.num_host_vertices, + 0, + ) } } diff --git a/src/rules/subsetsum_closestvectorproblem.rs b/src/rules/subsetsum_closestvectorproblem.rs index 0799edee4..7fa9986c4 100644 --- a/src/rules/subsetsum_closestvectorproblem.rs +++ b/src/rules/subsetsum_closestvectorproblem.rs @@ -25,6 +25,8 @@ impl ReductionResult for ReductionSubsetSumToClosestVectorProblem { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/subsetsum_integerexpressionmembership.rs b/src/rules/subsetsum_integerexpressionmembership.rs index 5244b4af1..ba0f37461 100644 --- a/src/rules/subsetsum_integerexpressionmembership.rs +++ b/src/rules/subsetsum_integerexpressionmembership.rs @@ -21,6 +21,8 @@ impl ReductionResult for ReductionSubsetSumToIntegerExpressionMembership { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ // Union choice 0 = left = Atom(1) = exclude, choice 1 = right = Atom(s_i+1) = include. // This maps directly to SubsetSum's 0/1 include/exclude encoding. diff --git a/src/rules/subsetsum_partition.rs b/src/rules/subsetsum_partition.rs index baf58dbb3..60f213de1 100644 --- a/src/rules/subsetsum_partition.rs +++ b/src/rules/subsetsum_partition.rs @@ -34,6 +34,8 @@ impl ReductionResult for ReductionSubsetSumToPartition { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let source_bits = &target_solution[..self.source_len]; diff --git a/src/rules/sumofsquarespartition_ilp.rs b/src/rules/sumofsquarespartition_ilp.rs index 48f47f8f1..7259c96b8 100644 --- a/src/rules/sumofsquarespartition_ilp.rs +++ b/src/rules/sumofsquarespartition_ilp.rs @@ -60,19 +60,14 @@ impl ReductionResult for ReductionSSPToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - Ok({ - let num_groups = self.num_groups; - (0..self.num_elements) - .map(|i| { - (0..num_groups) - .find(|&g| { - let idx = i * num_groups + g; - idx < target_solution.len() && target_solution[idx] == 1 - }) - .unwrap_or(0) - }) - .collect() - }) + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + + crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.num_elements, + self.num_groups, + 0, + ) } } diff --git a/src/rules/test_helpers.rs b/src/rules/test_helpers.rs index ef7e066bc..cb95999c3 100644 --- a/src/rules/test_helpers.rs +++ b/src/rules/test_helpers.rs @@ -297,6 +297,8 @@ mod tests { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } @@ -317,6 +319,8 @@ mod tests { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } @@ -337,6 +341,8 @@ mod tests { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } @@ -357,6 +363,8 @@ mod tests { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/threedimensionalmatching_ilp.rs b/src/rules/threedimensionalmatching_ilp.rs index 444838dc7..cf5bcb7ae 100644 --- a/src/rules/threedimensionalmatching_ilp.rs +++ b/src/rules/threedimensionalmatching_ilp.rs @@ -22,6 +22,8 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/threedimensionalmatching_minimumweightdecoding.rs b/src/rules/threedimensionalmatching_minimumweightdecoding.rs index d7a7e097f..89328ccf2 100644 --- a/src/rules/threedimensionalmatching_minimumweightdecoding.rs +++ b/src/rules/threedimensionalmatching_minimumweightdecoding.rs @@ -51,13 +51,8 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToMinimumWeightDecodin &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { - let expected = self.target.num_cols(); - if target_solution.len() != expected { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {expected} codeword values, got {}", - target_solution.len() - ))); - } + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..self.source_num_triples].to_vec()) } } diff --git a/src/rules/threedimensionalmatching_threematroidintersection.rs b/src/rules/threedimensionalmatching_threematroidintersection.rs index 4a6438dc0..2bcd603e5 100644 --- a/src/rules/threedimensionalmatching_threematroidintersection.rs +++ b/src/rules/threedimensionalmatching_threematroidintersection.rs @@ -24,6 +24,8 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToThreeMatroidIntersec &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/threedimensionalmatching_threepartition.rs b/src/rules/threedimensionalmatching_threepartition.rs index b3ff5f9a2..b94a13004 100644 --- a/src/rules/threedimensionalmatching_threepartition.rs +++ b/src/rules/threedimensionalmatching_threepartition.rs @@ -298,6 +298,8 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToThreePartition { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let mut groups = vec![Vec::new(); self.target.num_groups()]; for (element_index, &group_index) in target_solution.iter().enumerate() { diff --git a/src/rules/threepartition_resourceconstrainedscheduling.rs b/src/rules/threepartition_resourceconstrainedscheduling.rs index 7cf07c2d5..5881866f1 100644 --- a/src/rules/threepartition_resourceconstrainedscheduling.rs +++ b/src/rules/threepartition_resourceconstrainedscheduling.rs @@ -42,6 +42,8 @@ impl ReductionResult for ReductionThreePartitionToRCS { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs b/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs index 39e9227c5..6c8c14222 100644 --- a/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs +++ b/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs @@ -52,11 +52,17 @@ impl ReductionResult for ReductionThreePartitionToSRTD { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.target.num_tasks(); // Decode Lehmer code to permutation - let schedule = crate::models::misc::decode_lehmer(target_solution, n) - .expect("target_solution must be a valid Lehmer code"); + let schedule = + crate::models::misc::decode_lehmer(target_solution, n).ok_or_else(|| { + crate::rules::ExtractionError::invalid( + "target configuration is not a Lehmer code", + ) + })?; // Simulate the schedule to find start times let mut current_time: u64 = 0; diff --git a/src/rules/timetabledesign_ilp.rs b/src/rules/timetabledesign_ilp.rs index db2882ef4..8a7033f73 100644 --- a/src/rules/timetabledesign_ilp.rs +++ b/src/rules/timetabledesign_ilp.rs @@ -32,6 +32,8 @@ impl ReductionResult for ReductionTDToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/traits.rs b/src/rules/traits.rs index f6403f5e3..9465cc139 100644 --- a/src/rules/traits.rs +++ b/src/rules/traits.rs @@ -38,6 +38,34 @@ impl ExtractionError { pub type ExtractionResult = std::result::Result; +/// Validate that a target configuration matches its declared discrete space. +pub(crate) fn validate_target_solution( + target: &P, + solution: &[usize], +) -> ExtractionResult<()> { + let dims = target.dims(); + if solution.len() != dims.len() { + return Err(ExtractionError::invalid(format!( + "expected {} target values, got {}", + dims.len(), + solution.len() + ))); + } + + if let Some((index, (&value, &dimension))) = solution + .iter() + .zip(&dims) + .enumerate() + .find(|(_, (value, dimension))| value >= dimension) + { + return Err(ExtractionError::invalid(format!( + "target value {value} at position {index} is outside dimension {dimension}" + ))); + } + + Ok(()) +} + /// Result of reducing a source problem to a target problem. /// /// This trait encapsulates the target problem and provides methods @@ -157,6 +185,8 @@ impl ReductionResult for ReductionAutoCast { } fn extract_solution(&self, target_solution: &[usize]) -> ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution.to_vec()) } } diff --git a/src/rules/travelingsalesman_ilp.rs b/src/rules/travelingsalesman_ilp.rs index 022b946f6..308f786a2 100644 --- a/src/rules/travelingsalesman_ilp.rs +++ b/src/rules/travelingsalesman_ilp.rs @@ -8,6 +8,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::TravelingSalesman; use crate::reduction; +use crate::rules::ilp_helpers::one_hot_decode; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::{Graph, SimpleGraph}; @@ -21,13 +22,6 @@ pub struct ReductionTSPToILP { source_edges: Vec<(usize, usize)>, } -impl ReductionTSPToILP { - /// Variable index for x_{v,k}: vertex v at position k. - fn x_index(&self, v: usize, k: usize) -> usize { - v * self.num_vertices + k - } -} - impl ReductionResult for ReductionTSPToILP { type Source = TravelingSalesman; type Target = ILP; @@ -42,32 +36,28 @@ impl ReductionResult for ReductionTSPToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_vertices; - // Read tour: for each position k, find vertex v with x_{v,k} = 1 - let mut tour = vec![0usize; n]; - for k in 0..n { - for v in 0..n { - if target_solution[self.x_index(v, k)] == 1 { - tour[k] = v; - break; - } - } - } + let tour = one_hot_decode(target_solution, n, n, 0)?; // Map tour to edge selection let mut edge_selection = vec![0usize; self.source_edges.len()]; for k in 0..n { let u = tour[k]; let v = tour[(k + 1) % n]; - // Find the edge index for (u, v) or (v, u) - for (idx, &(a, b)) in self.source_edges.iter().enumerate() { - if (a == u && b == v) || (a == v && b == u) { - edge_selection[idx] = 1; - break; - } - } + let edge = self + .source_edges + .iter() + .position(|&(a, b)| (a == u && b == v) || (a == v && b == u)) + .ok_or_else(|| { + crate::rules::ExtractionError::invalid(format!( + "target tour uses absent source edge ({u}, {v})" + )) + })?; + edge_selection[edge] = 1; } edge_selection diff --git a/src/rules/travelingsalesman_qubo.rs b/src/rules/travelingsalesman_qubo.rs index d61795290..20093c505 100644 --- a/src/rules/travelingsalesman_qubo.rs +++ b/src/rules/travelingsalesman_qubo.rs @@ -9,6 +9,7 @@ use crate::models::algebraic::QUBO; use crate::models::graph::TravelingSalesman; use crate::reduction; +use crate::rules::ilp_helpers::one_hot_decode; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::{Graph, SimpleGraph}; use std::collections::HashMap; @@ -38,19 +39,12 @@ impl ReductionResult for ReductionTravelingSalesmanToQUBO { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let n = self.num_vertices; - // For each position p, find the vertex v where x_{v,p} == 1 - let mut tour = vec![0usize; n]; - for p in 0..n { - for v in 0..n { - if target_solution[v * n + p] == 1 { - tour[p] = v; - break; - } - } - } + let tour = one_hot_decode(target_solution, n, n, 0)?; // Build edge-based config: for each consecutive pair in the tour, mark the edge let mut config = vec![0usize; self.num_edges]; @@ -58,9 +52,12 @@ impl ReductionResult for ReductionTravelingSalesmanToQUBO { let u = tour[p]; let v = tour[(p + 1) % n]; let key = (u.min(v), u.max(v)); - if let Some(&idx) = self.edge_index.get(&key) { - config[idx] = 1; - } + let &edge = self.edge_index.get(&key).ok_or_else(|| { + crate::rules::ExtractionError::invalid(format!( + "target tour uses absent source edge ({u}, {v})" + )) + })?; + config[edge] = 1; } config diff --git a/src/rules/undirectedflowlowerbounds_ilp.rs b/src/rules/undirectedflowlowerbounds_ilp.rs index 00b9afe3b..81b3d13a1 100644 --- a/src/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/rules/undirectedflowlowerbounds_ilp.rs @@ -58,6 +58,8 @@ impl ReductionResult for ReductionUFLBToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok({ let e = self.num_edges; target_solution[2 * e..3 * e] diff --git a/src/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/rules/undirectedtwocommodityintegralflow_ilp.rs index 2521dcd13..5238299d8 100644 --- a/src/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -55,6 +55,8 @@ impl ReductionResult for ReductionU2CIFToILP { &self, target_solution: &[usize], ) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + Ok(target_solution[..4 * self.num_edges].to_vec()) } } diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index 053ec6f23..6b7fdd95c 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -697,6 +697,29 @@ fn rule_specs_solution_pairs_are_consistent() { (extracted: {:?}, stored: {:?})", extracted_val, source_val, extracted, pair.source_config ); + + let mut wrong_length = pair.target_config.clone(); + if wrong_length.is_empty() { + wrong_length.push(0); + } else { + wrong_length.pop(); + } + assert!( + chain.extract_solution(&wrong_length).is_err(), + "Rule {label}: extraction accepted a target configuration with the wrong length" + ); + + let target_dims = target.dims_dyn(); + if let Some((&dimension, value)) = + target_dims.first().zip(pair.target_config.first()) + { + let mut out_of_domain = pair.target_config.clone(); + out_of_domain[0] = dimension; + assert!( + chain.extract_solution(&out_of_domain).is_err(), + "Rule {label}: extraction accepted out-of-domain value {dimension} in place of {value}" + ); + } } } } diff --git a/src/unit_tests/rules/ilp_helpers.rs b/src/unit_tests/rules/ilp_helpers.rs index 40eda271b..7e157ba08 100644 --- a/src/unit_tests/rules/ilp_helpers.rs +++ b/src/unit_tests/rules/ilp_helpers.rs @@ -126,7 +126,7 @@ fn test_one_hot_decode_permutation() { solution[2] = 1; // item 0 -> slot 2 solution[3] = 1; // item 1 -> slot 0 solution[7] = 1; // item 2 -> slot 1 - let decoded = one_hot_decode(&solution, 3, 3, 0); + let decoded = one_hot_decode(&solution, 3, 3, 0).unwrap(); assert_eq!(decoded, vec![1, 2, 0]); // slot 0 gets item 1, slot 1 gets item 2, slot 2 gets item 0 } @@ -137,10 +137,27 @@ fn test_one_hot_decode_with_offset() { solution[7] = 1; // 5 + 2 solution[8] = 1; // 5 + 3 solution[12] = 1; // 5 + 7 - let decoded = one_hot_decode(&solution, 3, 3, 5); + let decoded = one_hot_decode(&solution, 3, 3, 5).unwrap(); assert_eq!(decoded, vec![1, 2, 0]); } +#[test] +fn test_one_hot_decode_rejects_missing_and_duplicate_items() { + assert!(one_hot_decode(&[0, 0, 0, 0], 2, 2, 0).is_err()); + assert!(one_hot_decode(&[1, 0, 1, 0], 2, 2, 0).is_err()); + assert!(one_hot_decode(&[1, 1, 0, 0], 2, 2, 0).is_err()); +} + +#[test] +fn test_one_hot_decode_rows_accepts_exactly_one_column_per_row() { + assert_eq!( + one_hot_decode_rows(&[0, 1, 0, 1, 0, 0], 2, 3, 0).unwrap(), + vec![1, 0] + ); + assert!(one_hot_decode_rows(&[0, 0, 0, 1, 0, 0], 2, 3, 0).is_err()); + assert!(one_hot_decode_rows(&[1, 1, 0, 1, 0, 0], 2, 3, 0).is_err()); +} + #[test] fn test_permutation_to_lehmer() { // Identity permutation [0,1,2] -> Lehmer [0,0,0] diff --git a/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs b/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs index 157e3ccf5..cfc642a1f 100644 --- a/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs +++ b/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs @@ -25,6 +25,17 @@ fn test_ksatisfiability_to_acyclicpartition_closed_loop() { } } +#[test] +fn test_partition_to_acyclicpartition_rejects_malformed_target_configuration() { + let source = KSatisfiability::::new(1, vec![CNFClause::new(vec![1, 1, 1])]); + let reduction = ReduceTo::>::reduce_to(&source); + + assert!(reduction + .partition_to_acyclic + .extract_solution(&[]) + .is_err()); +} + #[test] fn test_ksatisfiability_to_acyclicpartition_unsatisfiable() { let source = KSatisfiability::::new( diff --git a/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs b/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs index fe01aa0c9..36088f494 100644 --- a/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs +++ b/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs @@ -102,6 +102,15 @@ fn test_ksatisfiability_to_quadraticcongruences_extracts_assignment_from_constru ); } +#[test] +fn test_ksatisfiability_to_quadraticcongruences_rejects_missing_variable_signs() { + let source = yes_source(); + let reduction = ReduceTo::::reduce_to(&source); + let target_config = vec![0; reduction.target_problem().dims().len()]; + + assert!(reduction.extract_solution(&target_config).is_err()); +} + #[test] fn test_ksatisfiability_to_quadraticcongruences_closed_loop() { let source = KSatisfiability::::new(3, vec![CNFClause::new(vec![1, 2, -3])]); diff --git a/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs b/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs index 0dd22474b..a110ce603 100644 --- a/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs +++ b/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs @@ -98,6 +98,7 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_edgeless_s let arrangement = reduction.extract_solution(&witness).unwrap(); assert_eq!(arrangement.len(), 3); assert_eq!(source.evaluate(&arrangement), Or(true)); + assert!(reduction.extract_solution(&[]).is_err()); } #[test] @@ -128,6 +129,7 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_negative_b BruteForce::new().find_witness(&source).is_none(), "P_6 has no arrangement of length <= 4" ); + assert!(reduction.extract_solution(&[]).is_err()); } #[test] @@ -140,7 +142,7 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_extract_in .extract_solution(&[0, 1, 2]) .unwrap_err() .to_string(), - "expected a permutation of 6 columns, got 3 entries" + "expected 6 target values, got 3" ); assert_eq!( reduction diff --git a/src/unit_tests/rules/sat_minimumdominatingset.rs b/src/unit_tests/rules/sat_minimumdominatingset.rs index 824d2d3c9..0dc10fd3e 100644 --- a/src/unit_tests/rules/sat_minimumdominatingset.rs +++ b/src/unit_tests/rules/sat_minimumdominatingset.rs @@ -136,10 +136,38 @@ fn test_extract_solution_too_many_selected() { let sat = Satisfiability::new(1, vec![CNFClause::new(vec![1])]); let reduction = ReduceTo::>::reduce_to(&sat); - let ds_sol = vec![1, 1, 1, 1]; + let ds_sol = vec![1, 1, 0, 0]; assert_eq!( reduction.extract_solution(&ds_sol).unwrap_err().to_string(), - "selected 4 dominating-set vertices for 1 source variables" + "variable 0 gadget must select exactly one vertex, got 2" + ); +} + +#[test] +fn test_extract_solution_rejects_unselected_variable_gadget() { + let sat = Satisfiability::new(1, vec![CNFClause::new(vec![1])]); + let reduction = ReduceTo::>::reduce_to(&sat); + + assert_eq!( + reduction + .extract_solution(&[0, 0, 0, 0]) + .unwrap_err() + .to_string(), + "variable 0 gadget must select exactly one vertex, got 0" + ); +} + +#[test] +fn test_extract_solution_rejects_selected_clause_vertex() { + let sat = Satisfiability::new(1, vec![CNFClause::new(vec![1])]); + let reduction = ReduceTo::>::reduce_to(&sat); + + assert_eq!( + reduction + .extract_solution(&[1, 0, 0, 1]) + .unwrap_err() + .to_string(), + "clause vertex 0 is selected" ); } diff --git a/src/unit_tests/rules/satisfiability_naesatisfiability.rs b/src/unit_tests/rules/satisfiability_naesatisfiability.rs index 53c60a1ec..6d0964346 100644 --- a/src/unit_tests/rules/satisfiability_naesatisfiability.rs +++ b/src/unit_tests/rules/satisfiability_naesatisfiability.rs @@ -76,10 +76,7 @@ fn test_solution_extraction_distinguishes_zero_assignment_from_malformed_input() assert_eq!(reduction.extract_solution(&[0, 0, 0]).unwrap(), vec![0, 0]); let error = reduction.extract_solution(&[0, 0]).unwrap_err(); - assert_eq!( - error.to_string(), - "expected 3 values including the sentinel, got 2" - ); + assert_eq!(error.to_string(), "expected 3 target values, got 2"); assert!(reduction.extract_solution(&[0, 0, 0, 0]).is_err()); assert!(reduction.extract_solution(&[0, 2, 0]).is_err()); } diff --git a/src/unit_tests/rules/traits.rs b/src/unit_tests/rules/traits.rs index b26e3c30a..becdf7b29 100644 --- a/src/unit_tests/rules/traits.rs +++ b/src/unit_tests/rules/traits.rs @@ -4,8 +4,8 @@ fn test_traits_compile() { } use crate::rules::traits::{ - AggregateReductionResult, DynAggregateReductionResult, ReduceTo, ReduceToAggregate, - ReductionResult, + validate_target_solution, AggregateReductionResult, DynAggregateReductionResult, ReduceTo, + ReduceToAggregate, ReductionResult, }; use crate::traits::Problem; use crate::types::Sum; @@ -81,6 +81,16 @@ fn test_reduction() { assert_eq!(result.extract_solution(&[1, 0]).unwrap(), vec![1, 0]); } +#[test] +fn target_solution_validation_rejects_shape_and_domain_errors() { + let target = TargetProblem; + + assert!(validate_target_solution(&target, &[1, 0]).is_ok()); + assert!(validate_target_solution(&target, &[1]).is_err()); + assert!(validate_target_solution(&target, &[1, 0, 0]).is_err()); + assert!(validate_target_solution(&target, &[1, 2]).is_err()); +} + #[derive(Clone)] struct AggregateSourceProblem;