diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 8a86664ea..f90fd11b2 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -87,9 +87,9 @@ make release V=x.y.z # Tag and push a new release (CI publishes to crates.io) - `misc/` - Unique input structures - Run `pred list` for the full catalog of problems, variants, and reductions; `pred show ` for details on a specific problem - `src/rules/` - Reduction rules + inventory registration -- `src/solvers/` - BruteForce solver, ILP solver (feature-gated). To check if a problem supports ILP solving (via reduction path), run `pred path ILP` -- `src/traits.rs` - `Problem`, `OptimizationProblem`, `SatisfactionProblem` traits -- `src/rules/traits.rs` - `ReduceTo`, `ReductionResult` traits +- `src/solvers/` - BruteForce solver for aggregate values plus witness recovery when supported, ILP solver (feature-gated, witness-only). To check if a problem supports ILP solving via a witness-capable reduction path, run `pred path ILP` +- `src/traits.rs` - `Problem` trait +- `src/rules/traits.rs` - `ReduceTo`, `ReduceToAggregate`, `ReductionResult`, `AggregateReductionResult` traits - `src/registry/` - Compile-time reduction metadata collection - `problemreductions-cli/` - `pred` CLI tool (separate crate in workspace) - `src/unit_tests/` - Unit test files (mirroring `src/` structure, referenced via `#[path]`) @@ -104,36 +104,33 @@ make release V=x.y.z # Tag and push a new release (CI publishes to crates.io) Problem (core trait — all problems must implement) │ ├── const NAME: &'static str // e.g., "MaximumIndependentSet" -├── type Metric: Clone // SolutionSize for optimization, bool for satisfaction +├── type Value: Clone // aggregate value: Max/Min/Sum/Or/And/Extremum/... ├── fn dims(&self) -> Vec // config space: [2, 2, 2] for 3 binary variables -├── fn evaluate(&self, config) -> Metric +├── fn evaluate(&self, config) -> Value ├── fn variant() -> Vec<(&str, &str)> // e.g., [("graph","SimpleGraph"), ("weight","i32")] ├── fn num_variables(&self) -> usize // default: dims().len() └── fn problem_type() -> ProblemType // catalog bridge: registry lookup by NAME +``` -OptimizationProblem : Problem> (extension for optimization) -│ -├── type Value: PartialOrd + Clone // inner objective type (i32, f64, etc.) -└── fn direction(&self) -> Direction // Maximize or Minimize +**Witness-capable objective problems** (e.g., `MaximumIndependentSet`) typically use `Value = Max`, `Min`, or `Extremum`. -SatisfactionProblem : Problem (marker trait for decision problems) -``` +**Witness-capable feasibility problems** (e.g., `Satisfiability`) typically use `Value = Or`. -**Satisfaction problems** (e.g., `Satisfiability`) use `Metric = bool` and implement `SatisfactionProblem`. +**Aggregate-only problems** use fold values such as `Sum` or `And`; these solve to a value but have no representative witness configuration. -**Optimization problems** (e.g., `MaximumIndependentSet`) use `Metric = SolutionSize` where: +Common aggregate wrappers live in `src/types.rs`: ```rust -enum SolutionSize { Valid(T), Invalid } // Invalid = infeasible config -enum Direction { Maximize, Minimize } +Max, Min, Sum, Or, And, Extremum, ExtremumSense ``` ### Key Patterns - `variant_params!` macro implements `Problem::variant()` — e.g., `crate::variant_params![G, W]` for two type params, `crate::variant_params![]` for none (see `src/variant.rs`) -- `declare_variants!` proc macro registers concrete type instantiations with best-known complexity and registry-backed dynamic dispatch metadata — every entry must specify `opt` or `sat`, and one entry per problem may be marked `default` (see `src/models/graph/maximum_independent_set.rs`). Variable names in complexity strings are validated at compile time against actual getter methods. +- `declare_variants!` proc macro registers concrete type instantiations with best-known complexity and registry-backed load/serialize/value-solve/witness-solve metadata. One entry per problem may be marked `default`, and variable names in complexity strings are validated at compile time against actual getter methods. - Problems parameterized by graph type `G` and optionally weight type `W` (problem-dependent) -- `ReductionResult` provides `target_problem()` and `extract_solution()` -- `Solver::find_best()` → `Option>` for optimization problems; `Solver::find_satisfying()` → `Option>` for `Metric = bool` -- `BruteForce::find_all_best()` / `find_all_satisfying()` return `Vec>` for all optimal/satisfying solutions +- `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 +- 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 - `WeightElement` trait: `type Sum: NumericSize` + `fn to_sum(&self)` — converts weight to a summable numeric type @@ -170,10 +167,12 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: - Default variant ranking: `SimpleGraph`, `One`, `KN` are considered default values; variants with the most default values sort first - Nodes come exclusively from `#[reduction]` registrations; natural edges between same-name variants are inferred from the graph/weight subtype partial order - Each primitive reduction is determined by the exact `(source_variant, target_variant)` endpoint pair -- `#[reduction]` accepts only `overhead = { ... }` +- Reduction edges carry `EdgeCapabilities { witness, aggregate }`; graph search defaults to witness mode, and aggregate mode is available through `ReductionMode::Aggregate` +- `#[reduction]` accepts only `overhead = { ... }` and currently registers witness/config reductions; aggregate-only edges require manual `ReductionEntry` registration with `reduce_aggregate_fn` ### Extension Points - New models register dynamic load/serialize/brute-force dispatch through `declare_variants!` in the model file, not by adding manual match arms in the CLI +- Aggregate-only models are first-class in `declare_variants!`; aggregate-only 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` UX lives in `problemreductions-cli/src/commands/create.rs` - Canonical paper and CLI examples live in `src/example_db/model_builders.rs` and `src/example_db/rule_builders.rs` @@ -199,8 +198,8 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: **Reference implementations — read these first:** - **Reduction test:** `src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs` — closed-loop pattern - **Model test:** `src/unit_tests/models/graph/maximum_independent_set.rs` — evaluation, serialization -- **Solver test:** `src/unit_tests/solvers/brute_force.rs` — `find_best` + `find_satisfying` -- **Trait definitions:** `src/traits.rs` (`Problem`, `OptimizationProblem`), `src/solvers/mod.rs` (`Solver`) +- **Solver test:** `src/unit_tests/solvers/brute_force.rs` — aggregate `solve()` plus witness recovery helpers +- **Trait definitions:** `src/traits.rs` (`Problem`), `src/solvers/mod.rs` (`Solver`) ### Coverage diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md index 38fa0bad7..960b8b821 100644 --- a/.claude/skills/add-model/SKILL.md +++ b/.claude/skills/add-model/SKILL.md @@ -17,16 +17,16 @@ Before any implementation, collect all required information. If called from `iss |---|------|-------------|---------| | 1 | **Problem name** | Struct name with optimization prefix | `MaximumClique`, `MinimumDominatingSet` | | 2 | **Mathematical definition** | Formal definition with objective/constraints | "Given graph G=(V,E), find max-weight subset S where all pairs in S are adjacent" | -| 3 | **Problem type** | Optimization (maximize/minimize) or satisfaction | Optimization (Maximize) | +| 3 | **Problem type** | Objective (`Max`/`Min`), witness (`bool`), or aggregate-only (`Sum`/`And`/custom `Aggregate`) | Objective (Maximize) | | 4 | **Type parameters** | Graph type `G`, weight type `W`, or other | `G: Graph`, `W: WeightElement` | | 5 | **Struct fields** | What the struct holds | `graph: G`, `weights: Vec` | | 6 | **Configuration space** | What `dims()` returns | `vec![2; num_vertices]` for binary vertex selection | | 7 | **Feasibility check** | How to validate a configuration | "All selected vertices must be pairwise adjacent" | -| 8 | **Objective function** | How to compute the metric | "Sum of weights of selected vertices" | +| 8 | **Per-configuration value** | How `evaluate()` computes the aggregate contribution | "Return `Max(Some(total_weight))` for feasible configs" | | 9 | **Best known exact algorithm** | Complexity with variable definitions | "O(1.1996^n) by Xiao & Nagamochi (2017), where n = \|V\|" | | 10 | **Solving strategy** | How it can be solved | "BruteForce works; ILP reduction available" | | 11 | **Category** | Which sub-module under `src/models/` | `graph`, `formula`, `set`, `algebraic`, `misc` | -| 12 | **Expected outcome from the issue** | Concrete outcome for the issue's example instance | Optimization: one optimal solution + optimal value. Satisfaction: one valid/satisfying solution + why it is valid | +| 12 | **Expected outcome from the issue** | Concrete outcome for the issue's example instance | Objective: one optimal solution + optimal value. Witness: one valid/satisfying solution + why it is valid. Aggregate-only: the final aggregate value and how it is derived | If any item is missing, ask the user to provide it. Do NOT proceed until the checklist is complete. @@ -61,7 +61,7 @@ Read these first to understand the patterns: - **Optimization problem:** `src/models/graph/maximum_independent_set.rs` - **Satisfaction problem:** `src/models/formula/sat.rs` - **Model tests:** `src/unit_tests/models/graph/maximum_independent_set.rs` -- **Trait definitions:** `src/traits.rs` (`Problem`, `OptimizationProblem`, `SatisfactionProblem`) +- **Trait definitions / aggregate types:** `src/traits.rs` (`Problem`), `src/types.rs` (`Aggregate`, `Max`, `Min`, `Sum`, `Or`, `And`, `Extremum`) - **Registry dispatch boundary:** `src/registry/mod.rs`, `src/registry/variant.rs` - **CLI aliases:** `problemreductions-cli/src/problem_name.rs` - **CLI creation:** `problemreductions-cli/src/commands/create.rs` @@ -71,7 +71,8 @@ Read these first to understand the patterns: Before implementing, make sure the plan explicitly covers these items that structural review checks later: - `ProblemSchemaEntry` metadata is complete for the current schema shape (`display_name`, `aliases`, `dimensions`, and constructor-facing `fields`) -- `declare_variants!` is present with the correct `opt`/`sat` marker and exactly one `default` variant when multiple concrete variants exist +- `Problem::Value` uses the correct aggregate wrapper and witness support is intentional +- `declare_variants!` is present with exactly one `default` variant when multiple concrete variants exist - CLI discovery and `pred create ` support are included where applicable - A canonical model example is registered for example-db / `pred create --example` - `docs/paper/reductions.typ` adds both the display-name dictionary entry and the `problem-def(...)` @@ -110,19 +111,20 @@ Create `src/models//.rs`: // 1. inventory::submit! for ProblemSchemaEntry // 2. Struct definition with #[derive(Debug, Clone, Serialize, Deserialize)] // 3. Constructor (new) + accessor methods -// 4. Problem trait impl (NAME, Metric, dims, evaluate, variant) -// 5. OptimizationProblem or SatisfactionProblem impl -// 6. #[cfg(test)] #[path = "..."] mod tests; +// 4. Problem trait impl (NAME, Value, dims, evaluate, variant) +// 5. #[cfg(test)] #[path = "..."] mod tests; ``` Key decisions: - **Schema metadata:** `ProblemSchemaEntry` must reflect the current registry schema shape, including `display_name`, `aliases`, `dimensions`, and constructor-facing `fields` -- **Optimization problems:** `type Metric = SolutionSize`, implement `OptimizationProblem` with `direction()` -- **Satisfaction problems:** `type Metric = bool`, implement `SatisfactionProblem` (marker trait) +- **Objective problems:** use `type Value = Max<_>`, `Min<_>`, or `Extremum<_>` when the model should expose optimization-style witness helpers +- **Witness problems:** use `type Value = Or` for existential feasibility problems +- **Aggregate-only problems:** use a value-only aggregate such as `Sum<_>`, `And`, or a custom `Aggregate` when witnesses are not meaningful - **Weight management:** use inherent methods (`weights()`, `set_weights()`, `is_weighted()`), NOT traits - **`dims()`:** returns the configuration space dimensions (e.g., `vec![2; n]` for binary variables) -- **`evaluate()`:** must check feasibility first, then compute objective +- **`evaluate()`:** must return the per-configuration aggregate value. For models with invalid configs, check feasibility first and return the appropriate invalid/false contribution - **`variant()`:** use the `variant_params!` macro — e.g., `crate::variant_params![G, W]` for `Problem`, or `crate::variant_params![]` for problems with no type parameters. Each type parameter must implement `VariantParam` (already done for standard types like `SimpleGraph`, `i32`, `One`). See `src/variant.rs`. +- **Solve surface:** `Solver::solve()` always computes the aggregate value. `pred solve problem.json` prints a `Solution` only when a witness exists; `pred solve bundle.json` and `--solver ilp` remain witness-only workflows ## Step 2.5: Register variant complexity @@ -130,14 +132,11 @@ Add `declare_variants!` at the bottom of the model file (after the trait impls, ```rust crate::declare_variants! { - opt ProblemName => "1.1996^num_vertices", - default opt ProblemName => "1.1996^num_vertices", + ProblemName => "1.1996^num_vertices", + default ProblemName => "1.1996^num_vertices", } ``` -- Each entry must include an explicit solver kind: - - `opt` for optimization problems (`BruteForce::find_best`) - - `sat` for satisfaction problems (`BruteForce::find_satisfying`) - Mark exactly one concrete variant `default` when the problem has multiple registered variants - The complexity string references the getter method names from Step 1.5 (e.g., `num_vertices`) — variable names are validated at compile time against actual getters, so typos cause compile errors - One entry per supported `(graph, weight)` combination @@ -146,6 +145,8 @@ crate::declare_variants! { - A compiled `complexity_eval_fn` plus registry-backed load/serialize/solve dispatch metadata are auto-generated alongside the symbolic expression - See `src/models/graph/maximum_independent_set.rs` for the reference pattern +`declare_variants!` now handles objective, witness-capable, and aggregate-only models uniformly. Use manual `VariantEntry` wiring only for unusual dynamic-registration work, not for ordinary models. + ## Step 3: Register the model Update these files to register the new problem type: @@ -160,7 +161,6 @@ The CLI now loads, serializes, and brute-force solves problems through the core 1. **Registry-backed dispatch comes from `declare_variants!`:** - Make sure every concrete variant you want the CLI to load is listed in `declare_variants!` - - Use the correct `opt`/`sat` marker per entry - Mark the intended default variant with `default` when applicable 2. **`problemreductions-cli/src/problem_name.rs`:** @@ -200,9 +200,9 @@ Create `src/unit_tests/models//.rs`: Every model needs **at least 3 test functions** (the structural reviewer enforces this). Choose from the coverage areas below — pick whichever are relevant to the model: - **Creation/basic** — exercise constructor inputs, key accessors, `dims()` / `num_variables()`. -- **Evaluation** — valid and invalid configs so the feasibility boundary is explicit. -- **Direction** — verify optimization direction (optimization problems only). -- **Solver** — brute-force solver finds correct solutions (when the model is small enough). +- **Evaluation** — valid and invalid configs so the feasibility boundary or aggregate contribution is explicit. +- **Direction / sense** — verify runtime optimization sense only for models that use `Extremum<_>`. +- **Solver** — brute-force `solve()` returns the correct aggregate value; if witnesses are supported, verify `find_witness()` / `find_all_witnesses()` as well. - **Serialization** — round-trip serde (when the model is used in CLI/example-db flows). - **Paper example** — verify the worked example from the paper entry (see below). @@ -280,9 +280,11 @@ Structural and quality review is handled by the `review-pipeline` stage, not her | Forgetting `inventory::submit!` | Every problem needs a `ProblemSchemaEntry` registration | | Missing `#[path]` test link | Add `#[cfg(test)] #[path = "..."] mod tests;` at file bottom | | Wrong `dims()` | Must match the actual configuration space (e.g., `vec![2; n]` for binary) | +| Using the wrong aggregate wrapper | Objective models use `Max` / `Min` / `Extremum`, witness models use `bool`, aggregate-only models use a fold value like `Sum` / `And` | | Not registering in `mod.rs` | Must update both `/mod.rs` and `models/mod.rs` | | Forgetting `declare_variants!` | Required for variant complexity metadata and registry-backed load/serialize/solve dispatch | -| Wrong `declare_variants!` syntax | Every entry now needs `opt` or `sat`; one entry per problem may be marked `default` | +| Wrong aggregate wrapper | Use `Max` / `Min` / `Extremum` for objective problems, `Or` for existential witness problems, and `Sum` / `And` (or a custom aggregate) for value-only folds | +| Wrong `declare_variants!` syntax | Entries no longer use `opt` / `sat`; one entry per problem may be marked `default` | | Forgetting CLI alias | Must add lowercase entry in `problem_name.rs` `resolve_alias()` | | Inventing short aliases | Only use well-established literature abbreviations (MIS, SAT, TSP); do NOT invent new ones | | Forgetting CLI create | Must add creation handler in `commands/create.rs` and flags in `cli.rs` | diff --git a/.claude/skills/add-rule/SKILL.md b/.claude/skills/add-rule/SKILL.md index 96dfedb73..9bff1abcc 100644 --- a/.claude/skills/add-rule/SKILL.md +++ b/.claude/skills/add-rule/SKILL.md @@ -33,7 +33,7 @@ Read these first to understand the patterns: - **Reduction rule:** `src/rules/minimumvertexcover_maximumindependentset.rs` - **Reduction tests:** `src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs` - **Paper entry:** search `docs/paper/reductions.typ` for `MinimumVertexCover` `MaximumIndependentSet` -- **Traits:** `src/rules/traits.rs` (`ReduceTo`, `ReductionResult`) +- **Traits:** `src/rules/traits.rs` (`ReduceTo`, `ReduceToAggregate`, `ReductionResult`, `AggregateReductionResult`) ## Step 1: Implement the reduction @@ -84,6 +84,8 @@ impl ReduceTo for SourceType { Each primitive reduction is determined by the exact source/target variant pair. Keep one primitive registration per endpoint pair and use only the `overhead` form of `#[reduction]`. +**Aggregate-only reductions:** when the rule preserves aggregate values but cannot recover a source witness from a target witness, implement `AggregateReductionResult` + `ReduceToAggregate` instead of `ReductionResult` + `ReduceTo`. Those edges are not auto-registered by `#[reduction]` yet; register them manually with `ReductionEntry { reduce_aggregate_fn: ..., capabilities: EdgeCapabilities::aggregate_only(), ... }`. See `src/unit_tests/rules/traits.rs` and `src/unit_tests/rules/graph.rs` for the reference pattern. + ## Step 2: Register in mod.rs Add to `src/rules/mod.rs`: @@ -98,7 +100,7 @@ Create `src/unit_tests/rules/_.rs`: ```rust // 1. Create source problem instance // 2. Reduce: let reduction = ReduceTo::::reduce_to(&source); -// 3. Solve target: solver.find_all_best(reduction.target_problem()) +// 3. Solve target: solver.find_all_witnesses(reduction.target_problem()) // 4. Extract: reduction.extract_solution(&target_sol) // 5. Verify: extracted solution is valid and optimal for source ``` @@ -108,6 +110,11 @@ Additional recommended tests: - Edge cases (empty graph, single vertex, etc.) - Weight preservation (if applicable) +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()` +- If testing a path, use `ReductionGraph::reduce_aggregate_along_path(...)` + Link via `#[cfg(test)] #[path = "..."] mod tests;` at the bottom of the rule file. ## Step 4: Add canonical example to example_db @@ -190,7 +197,12 @@ Structural and quality review is handled by the `review-pipeline` stage, not her ## CLI Impact -Adding a 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). +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). + +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 +- Manual aggregate-edge registration affects runtime graph search and internal value extraction, but not bundle solving ## File Naming @@ -204,6 +216,7 @@ Adding a reduction rule does NOT require CLI changes -- the reduction graph is a | Mistake | Fix | |---------|-----| | Forgetting `#[reduction(...)]` macro | Required for compile-time registration in the reduction graph | +| Using `#[reduction]` for an aggregate-only rule | `#[reduction]` currently registers witness/config edges only; aggregate-only rules need manual `ReductionEntry` wiring with `reduce_aggregate_fn` | | 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 | diff --git a/.claude/skills/final-review/SKILL.md b/.claude/skills/final-review/SKILL.md index bcb34d1cf..633d86ad8 100644 --- a/.claude/skills/final-review/SKILL.md +++ b/.claude/skills/final-review/SKILL.md @@ -217,7 +217,7 @@ Verify the PR includes all required components: **For [Model] PRs:** - [ ] Model implementation (`src/models/...`) - [ ] Unit tests (`src/unit_tests/models/...`) -- [ ] `declare_variants!` macro with explicit `opt`/`sat` solver-kind markers and intended default variant +- [ ] Variant registration exists: usually `declare_variants!` with the intended default variant, or justified manual `VariantEntry` wiring for unusual dynamic-registration work - [ ] Schema / registry entry for CLI-facing model creation (`ProblemSchemaEntry`) - [ ] `canonical_model_example_specs()` function in the model file (gated by `#[cfg(feature = "example-db")]`) and registered in the category `mod.rs` example chain - [ ] Paper section in `docs/paper/reductions.typ` (`problem-def` entry) diff --git a/.claude/skills/fix-issue/SKILL.md b/.claude/skills/fix-issue/SKILL.md index 0a2aa60f9..fe1d1d777 100644 --- a/.claude/skills/fix-issue/SKILL.md +++ b/.claude/skills/fix-issue/SKILL.md @@ -196,7 +196,7 @@ Tag each issue as: | Incorrect mathematical claims | Domain expertise needed | | Incomplete reduction algorithm | Core technical content | | Incomplete or trivial example | Present **3 concrete example options** with pros/cons (use `AskUserQuestion` with previews showing vertex/edge counts, optimal values, and suboptimal cases). Prefer examples that match the model issue's example when a companion model exists. | -| Decision vs optimization framing | **Default to optimization** unless evidence points otherwise. The project prefers `OptimizationProblem` (like MIS, SpinGlass, TSP) because optimization subsumes decision. Check associated `[Rule]` issues (`gh issue list --search " in:title label:rule"`) to see how rules use the model — if rules only need the decision version (e.g., reducing to SAT with a bound), optimization still works since you can extract the bound from the optimal value. Only use `SatisfactionProblem` for inherently decision/feasibility problems (SAT, KColoring) where there is no natural optimization objective. If switching to optimization, add the appropriate `Minimum`/`Maximum` prefix per codebase conventions. | +| Decision vs optimization framing | **Default to objective-style models** unless evidence points otherwise. In the current aggregate-value architecture, that usually means `type Value = Max<_>`, `Min<_>`, or `Extremum<_>` when the sense is runtime data. Check associated `[Rule]` issues (`gh issue list --search " in:title label:rule"`) to see how rules use this model — if rules only need the decision version (e.g., reducing to SAT with a bound), an objective model still works because the bound can be read from the optimal aggregate value. Use `Or` for inherently existential feasibility problems (SAT, KColoring) where there is no natural objective. Use aggregate-only values such as `Sum<_>` or `And` only when the answer is genuinely a fold over all configurations and there is no representative witness. If switching to an objective model, add the appropriate `Minimum`/`Maximum` prefix per codebase conventions. | | Ambiguous overhead expressions | Requires understanding the reduction | --- diff --git a/.claude/skills/review-structural/SKILL.md b/.claude/skills/review-structural/SKILL.md index 60daf1f45..c169ebcbd 100644 --- a/.claude/skills/review-structural/SKILL.md +++ b/.claude/skills/review-structural/SKILL.md @@ -54,13 +54,13 @@ Only run if review type includes "model". Given: problem name `P`, category `C`, | 2 | `inventory::submit!` present | `Grep("inventory::submit", file)` | | 3 | `#[derive(...Serialize, Deserialize)]` on struct | `Grep("Serialize.*Deserialize", file)` | | 4 | `Problem` trait impl | `Grep("impl.*Problem for.*{P}", file)` | -| 5 | `OptimizationProblem` or `SatisfactionProblem` impl | `Grep("(OptimizationProblem|SatisfactionProblem).*for.*{P}", file)` | +| 5 | Aggregate value is present | `Grep("type Value =", file)` | | 6 | `#[cfg(test)]` + `#[path = "..."]` test link | `Grep("#\\[path =", file)` | | 7 | Test file exists | `Glob("src/unit_tests/models/{C}/{F}.rs")` | | 8 | Test file has >= 3 test functions | `Grep("fn test_", test_file)` — count matches, FAIL if < 3 | | 9 | Registered in `{C}/mod.rs` | `Grep("mod {F}", "src/models/{C}/mod.rs")` | | 10 | Re-exported in `models/mod.rs` | `Grep("{P}", "src/models/mod.rs")` | -| 11 | `declare_variants!` entry exists | `Grep("declare_variants!|default opt|default sat|opt {P}|sat {P}", file)` | +| 11 | Variant registration exists | `Grep("declare_variants!|VariantEntry", file)` | | 12 | CLI `resolve_alias` entry | `Grep("{P}", "problemreductions-cli/src/problem_name.rs")` | | 13 | CLI `create` support | `Grep('"{P}"', "problemreductions-cli/src/commands/create.rs")` | | 14 | Canonical model example registered | `Grep("{P}", "src/example_db/model_builders.rs")` | @@ -107,7 +107,7 @@ Report pass/fail. If tests fail, identify which tests. **Do NOT fix anything** ## Step 4: Semantic Review ### For Models: -1. **`evaluate()` correctness** — Does it check feasibility before computing the objective? Does it return `SolutionSize::Invalid` / `false` for infeasible configs? +1. **`evaluate()` correctness** — Does it check feasibility before computing the objective when the model has invalid configurations? Objective models should return `Max/Min/Extremum(None)` for infeasible configs, witness problems should return `false`, and aggregate-only models should return the per-configuration contribution that matches the intended fold semantics. 2. **`dims()` correctness** — Does it return the actual configuration space? (e.g., `vec![2; n]` for binary) 3. **Size getter consistency** — Do inherent getter methods (e.g., `num_vertices()`, `num_edges()`) match names used in overhead expressions? 4. **Weight handling** — Are weights managed via inherent methods, not traits? @@ -127,7 +127,7 @@ Only if a linked issue was provided. |---|-------| | 1 | Problem name matches issue | | 2 | Mathematical definition matches | -| 3 | Problem type (opt/sat) matches | +| 3 | Problem framing (objective / witness / aggregate-only) matches | | 4 | Type parameters match | | 5 | Configuration space matches | | 6 | Feasibility check matches | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d05d6903f..65adf7b6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: run: cargo build --features ilp-highs --verbose - name: Run tests - run: cargo test --features "ilp-highs example-db" --verbose + run: cargo test --features "ilp-highs example-db" --workspace --verbose - name: Run doc tests run: cargo test --doc --features ilp-highs --verbose diff --git a/benches/solver_benchmarks.rs b/benches/solver_benchmarks.rs index 0ee082fc3..69572e1d5 100644 --- a/benches/solver_benchmarks.rs +++ b/benches/solver_benchmarks.rs @@ -21,7 +21,7 @@ fn bench_independent_set(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { - b.iter(|| solver.find_best(black_box(&problem))) + b.iter(|| solver.find_witness(black_box(&problem))) }); } @@ -38,7 +38,7 @@ fn bench_vertex_covering(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { - b.iter(|| solver.find_best(black_box(&problem))) + b.iter(|| solver.find_witness(black_box(&problem))) }); } @@ -56,7 +56,7 @@ fn bench_max_cut(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { - b.iter(|| solver.find_best(black_box(&problem))) + b.iter(|| solver.find_witness(black_box(&problem))) }); } @@ -83,7 +83,7 @@ fn bench_satisfiability(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("3-sat", num_vars), num_vars, |b, _| { - b.iter(|| solver.find_all_satisfying(black_box(&problem))) + b.iter(|| solver.find_all_witnesses(black_box(&problem))) }); } @@ -104,7 +104,7 @@ fn bench_spin_glass(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("chain", n), n, |b, _| { - b.iter(|| solver.find_best(black_box(&problem))) + b.iter(|| solver.find_witness(black_box(&problem))) }); } @@ -126,7 +126,7 @@ fn bench_set_covering(c: &mut Criterion) { group.bench_with_input( BenchmarkId::new("overlapping", num_sets), num_sets, - |b, _| b.iter(|| solver.find_best(black_box(&problem))), + |b, _| b.iter(|| solver.find_witness(black_box(&problem))), ); } @@ -143,7 +143,7 @@ fn bench_coloring(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path_3colors", n), n, |b, _| { - b.iter(|| solver.find_all_satisfying(black_box(&problem))) + b.iter(|| solver.find_all_witnesses(black_box(&problem))) }); } @@ -161,7 +161,7 @@ fn bench_matching(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { - b.iter(|| solver.find_best(black_box(&problem))) + b.iter(|| solver.find_witness(black_box(&problem))) }); } @@ -182,7 +182,7 @@ fn bench_paintshop(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("sequential", n), n, |b, _| { - b.iter(|| solver.find_best(black_box(&problem))) + b.iter(|| solver.find_witness(black_box(&problem))) }); } @@ -201,7 +201,7 @@ fn bench_comparison(c: &mut Criterion) { vec![1i32; 8], ); group.bench_function("MaximumIndependentSet", |b| { - b.iter(|| solver.find_best(black_box(&is_problem))) + b.iter(|| solver.find_witness(black_box(&is_problem))) }); // SAT with 8 variables @@ -215,7 +215,7 @@ fn bench_comparison(c: &mut Criterion) { ], ); group.bench_function("Satisfiability", |b| { - b.iter(|| solver.find_all_satisfying(black_box(&sat_problem))) + b.iter(|| solver.find_all_witnesses(black_box(&sat_problem))) }); // SpinGlass with 8 spins @@ -225,7 +225,7 @@ fn bench_comparison(c: &mut Criterion) { vec![0.0; 8], ); group.bench_function("SpinGlass", |b| { - b.iter(|| solver.find_best(black_box(&sg_problem))) + b.iter(|| solver.find_witness(black_box(&sg_problem))) }); // MaxCut with 8 vertices @@ -234,7 +234,7 @@ fn bench_comparison(c: &mut Criterion) { vec![1, 1, 1, 1], ); group.bench_function("MaxCut", |b| { - b.iter(|| solver.find_best(black_box(&mc_problem))) + b.iter(|| solver.find_witness(black_box(&mc_problem))) }); group.finish(); diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 5b94df3f3..87436d663 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -48,6 +48,20 @@ } } +#let metric-value(metric) = { + if type(metric) == dictionary { + if "Valid" in metric { + metric.Valid + } else if "value" in metric { + metric.value + } else { + metric + } + } else { + metric + } +} + #let graph-num-vertices(instance) = instance.graph.num_vertices #let graph-num-edges(instance) = instance.graph.edges.len() #let spin-num-spins(instance) = instance.fields.len() @@ -497,7 +511,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| // Pick optimal config = {v1, v3, v5, v9} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) let S = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let alpha = sol.metric.Valid + let alpha = metric-value(sol.metric) [ #problem-def("MaximumIndependentSet")[ Given $G = (V, E)$ with vertex weights $w: V -> RR$, find $S subset.eq V$ maximizing $sum_(v in S) w(v)$ such that no two vertices in $S$ are adjacent: $forall u, v in S: (u, v) in.not E$. @@ -530,7 +544,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| // Pick optimal config = {v0, v3, v4} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) let cover = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let wS = sol.metric.Valid + let wS = metric-value(sol.metric) let complement = sol.config.enumerate().filter(((i, v)) => v == 0).map(((i, _)) => i) let alpha = complement.len() [ @@ -569,7 +583,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let sol = (config: x.optimal_config, metric: x.optimal_value) let side-s = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) let side-sbar = sol.config.enumerate().filter(((i, v)) => v == 0).map(((i, _)) => i) - let cut-val = sol.metric.Valid + let cut-val = metric-value(sol.metric) let cut-edges = edges.filter(e => side-s.contains(e.at(0)) != side-s.contains(e.at(1))) let uncut-edges = edges.filter(e => side-s.contains(e.at(0)) == side-s.contains(e.at(1))) [ @@ -602,7 +616,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let ne = graph-num-edges(x.instance) let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1))) let config = x.optimal_config - let cut-val = x.optimal_value.Valid + let cut-val = metric-value(x.optimal_value) let side-a = range(nv).filter(i => config.at(i) == 0) let side-b = range(nv).filter(i => config.at(i) == 1) let cut-edges = edges.filter(e => config.at(e.at(0)) != config.at(e.at(1))) @@ -1579,7 +1593,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 // Pick optimal config = {v2, v3} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) let S = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let wS = sol.metric.Valid + let wS = metric-value(sol.metric) // Compute neighbors dominated by each vertex in S let dominated = S.map(s => { let nbrs = () @@ -1620,7 +1634,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 // Pick optimal config [1,0,0,0,1,0] = edges {(0,1),(2,4)} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) let matched-edges = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => edges.at(i)) - let wM = sol.metric.Valid + let wM = metric-value(sol.metric) // Collect matched vertices let matched-verts = () for (u, v) in matched-edges { @@ -1659,7 +1673,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 let ew = x.instance.edge_weights let sol = (config: x.optimal_config, metric: x.optimal_value) let tour-edges = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => edges.at(i)) - let bottleneck = sol.metric.Valid + let bottleneck = metric-value(sol.metric) let tour-weights = tour-edges.map(((u, v)) => { let idx = edges.position(e => e == (u, v) or e == (v, u)) int(ew.at(idx)) @@ -1749,7 +1763,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 let ew = x.instance.edge_weights let sol = (config: x.optimal_config, metric: x.optimal_value) let tour-edges = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => edges.at(i)) - let tour-cost = sol.metric.Valid + let tour-cost = metric-value(sol.metric) // Build ordered tour from tour-edges starting at vertex 0 let tour-order = (0,) let remaining = tour-edges @@ -1819,7 +1833,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 let sol = (config: x.optimal_config, metric: x.optimal_value) let tree-edge-indices = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) let tree-edges = tree-edge-indices.map(i => edges.at(i)) - let cost = sol.metric.Valid + let cost = metric-value(sol.metric) // Steiner vertices: in tree but not terminals let tree-verts = tree-edges.map(e => (e.at(0), e.at(1))).fold((), (acc, pair) => { let (u, v) = pair @@ -1967,7 +1981,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 let sol = (config: x.optimal_config, metric: x.optimal_value) let cut-edge-indices = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) let cut-edges = cut-edge-indices.map(i => edges.at(i)) - let cost = sol.metric.Valid + let cost = metric-value(sol.metric) [ #problem-def("MinimumMultiwayCut")[ Given an undirected graph $G=(V,E)$ with edge weights $w: E -> RR_(>0)$ and a set of $k$ terminal vertices $T = {t_1, ..., t_k} subset.eq V$, find a minimum-weight set of edges $C subset.eq E$ such that no two terminals remain in the same connected component of $G' = (V, E backslash C)$. @@ -2098,7 +2112,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 // optimal config = {v2, v3, v4} let sol = (config: x.optimal_config, metric: x.optimal_value) let K = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let omega = sol.metric.Valid + let omega = metric-value(sol.metric) // Edges within the clique let clique-edges = edges.filter(e => K.contains(e.at(0)) and K.contains(e.at(1))) [ @@ -2132,7 +2146,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 // optimal config = {v0,v2,v4} with w=3 (maximum-weight maximal IS) let opt = (config: x.optimal_config, metric: x.optimal_value) let S-opt = opt.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let w-opt = opt.metric.Valid + let w-opt = metric-value(opt.metric) // Suboptimal maximal IS {v1,v3} with w=2 (hardcoded — no longer in fixture) let S-sub = (1, 3) let w-sub = 2 @@ -2167,7 +2181,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 let sol = (config: x.optimal_config, metric: x.optimal_value) let merged = arcs.enumerate().filter(((i, _)) => sol.config.at(i) == 1).map(((i, arc)) => arc) let dummy = arcs.enumerate().filter(((i, _)) => sol.config.at(i) == 0).map(((i, arc)) => arc) - let opt = sol.metric.Valid + let opt = metric-value(sol.metric) let blue = graph-colors.at(0) [ #problem-def("MinimumDummyActivitiesPert")[ @@ -2219,7 +2233,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 // Pick optimal config = {v0} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) let S = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let wS = sol.metric.Valid + let wS = metric-value(sol.metric) [ #problem-def("MinimumFeedbackVertexSet")[ Given a directed graph $G = (V, A)$ with vertex weights $w: V -> RR$, find $S subset.eq V$ minimizing $sum_(v in S) w(v)$ such that the induced subgraph $G[V backslash S]$ is a directed acyclic graph (DAG). @@ -2272,7 +2286,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let terminals = x.instance.terminals let weights = x.instance.edge_weights let sol = (config: x.optimal_config, metric: x.optimal_value) - let opt-weight = sol.metric.Valid + let opt-weight = metric-value(sol.metric) // Derive tree edges from optimal config let tree-edge-indices = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) let tree-edges = tree-edge-indices.map(i => edges.at(i)) @@ -2327,7 +2341,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let nv = graph-num-vertices(x.instance) let edges = x.instance.graph.edges let K = x.instance.k - let opt-cost = x.optimal_value.Valid + let opt-cost = metric-value(x.optimal_value) // Pick optimal config = {v2, v5} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) let centers = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) @@ -2517,7 +2531,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], // Pick optimal config = {S1, S3} (0-indexed: sets 0, 2) to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) let selected = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let wP = sol.metric.Valid + let wP = metric-value(sol.metric) // Format a set as {e1+1, e2+1, ...} (1-indexed) let fmt-set(s) = "${" + s.map(e => str(e + 1)).join(", ") + "}$" [ @@ -2560,7 +2574,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let U-size = x.instance.universe_size let sol = (config: x.optimal_config, metric: x.optimal_value) let selected = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let wC = sol.metric.Valid + let wC = metric-value(sol.metric) let fmt-set(s) = "${" + s.map(e => str(e + 1)).join(", ") + "}$" [ #problem-def("MinimumSetCovering")[ @@ -2605,7 +2619,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let U-size = x.instance.universe_size let sol = (config: x.optimal_config, metric: x.optimal_value) let selected = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let hit-size = sol.metric.Valid + let hit-size = metric-value(sol.metric) let fmt-set(s) = if s.len() == 0 { $emptyset$ } else { @@ -3044,7 +3058,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let sol = (config: x.optimal_config, metric: x.optimal_value) // Convert config (0=+1, 1=-1) to spin values let spins = sol.config.map(v => if v == 0 { 1 } else { -1 }) - let H = sol.metric.Valid + let H = metric-value(sol.metric) let spin-str = spins.map(s => if s > 0 { "+" } else { "-" }).join(", ") // Count satisfied and frustrated edges let sat-count = edges.filter(((u, v)) => spins.at(u) * spins.at(v) < 0).len() @@ -3091,7 +3105,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let Q = x.instance.matrix let sol = (config: x.optimal_config, metric: x.optimal_value) let xstar = sol.config - let fstar = sol.metric.Valid + let fstar = metric-value(sol.metric) // Format the Q matrix as semicolon-separated rows let mat-rows = Q.map(row => row.map(v => { let vi = int(v) @@ -3131,7 +3145,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let constraints = x.instance.constraints let sol = (config: x.optimal_config, metric: x.optimal_value) let xstar = sol.config - let fstar = sol.metric.Valid + let fstar = metric-value(sol.metric) // Format objective: c1*x1 + c2*x2 + ... let fmt-obj = obj.map(((i, c)) => { let ci = int(c) @@ -3220,7 +3234,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let m = D.len() let sol = (config: x.optimal_config, metric: x.optimal_value) let fstar = sol.config - let cost-star = sol.metric.Valid + let cost-star = metric-value(sol.metric) // Convert integer matrix to math.mat content let to-mat(m) = math.mat(..m.map(row => row.map(v => $#v$))) // Compute identity assignment cost @@ -3311,7 +3325,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let target = x.instance.target let bounds = x.instance.bounds let sol = (config: x.optimal_config, metric: x.optimal_value) - let dist = sol.metric.Valid + let dist = metric-value(sol.metric) // Config encodes offset from lower bound; recover actual integer coordinates let coords = sol.config.enumerate().map(((i, v)) => v + bounds.at(i).lower) // Compute B*x: sum over j of coords[j] * basis[j] @@ -3677,7 +3691,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let nc = x.instance.n let k = x.instance.k let A = x.instance.matrix - let dH = x.optimal_value.Valid + let dH = metric-value(x.optimal_value) // Decode B and C from optimal config // Config layout: B is m*k values, then C is k*n values let cfg = x.optimal_config @@ -3776,7 +3790,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let is-first = x.instance.is_first let sol = (config: x.optimal_config, metric: x.optimal_value) let assign = sol.config // color assignment per car - let num-changes = sol.metric.Valid + let num-changes = metric-value(sol.metric) // Build the full sequence of car labels let seq-labels = seq-indices.map(i => labels.at(i)) // Build color sequence: for each position, if is_first[pos] then color = assign[car], else 1-assign[car] @@ -3830,7 +3844,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let bip-edges = x.instance.graph.edges // (li, rj) pairs let ne = bip-edges.len() let sol = (config: x.optimal_config, metric: x.optimal_value) - let total-size = sol.metric.Valid + let total-size = metric-value(sol.metric) [ #problem-def("BicliqueCover")[ Given a bipartite graph $G = (L, R, E)$ and integer $k$, find $k$ bicliques $(L_1, R_1), dots, (L_k, R_k)$ that cover all edges ($E subset.eq union.big_i L_i times R_i$) while minimizing the total size $sum_i (|L_i| + |R_i|)$. @@ -3993,7 +4007,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let n = sizes.len() let C = x.instance.capacity let config = x.optimal_config - let num-bins = x.optimal_value.Valid + let num-bins = metric-value(x.optimal_value) // Group items by bin let bins-contents = range(num-bins).map(b => range(n).filter(i => config.at(i) == b) @@ -4051,7 +4065,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let C = x.instance.capacity let n = weights.len() let config = x.optimal_config - let opt-val = x.optimal_value.Valid + let opt-val = metric-value(x.optimal_value) let selected = range(n).filter(i => config.at(i) == 1) let total-w = selected.map(i => weights.at(i)).sum() let total-v = selected.map(i => values.at(i)).sum() @@ -4849,7 +4863,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let na = arcs.len() let weights = x.instance.weights let config = x.optimal_config - let opt-val = x.optimal_value.Valid + let opt-val = metric-value(x.optimal_value) let removed = range(na).filter(i => config.at(i) == 1) [ #problem-def("MinimumFeedbackArcSet")[ @@ -5512,7 +5526,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let deadlines = x.instance.deadlines let precs = x.instance.precedences let sol = (config: x.optimal_config, metric: x.optimal_value) - let tardy-count = sol.metric.Valid + let tardy-count = metric-value(sol.metric) // Decode Lehmer code to permutation (schedule order) let lehmer = sol.config let schedule = { @@ -5595,7 +5609,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let precs = x.instance.precedences let ntasks = lengths.len() let sol = (config: x.optimal_config, metric: x.optimal_value) - let opt = sol.metric.Valid + let opt = metric-value(sol.metric) let lehmer = sol.config let schedule = { let avail = range(ntasks) diff --git a/docs/plans/2026-03-13-quantified-boolean-formulas-model.md b/docs/plans/2026-03-13-quantified-boolean-formulas-model.md deleted file mode 100644 index 767650d0a..000000000 --- a/docs/plans/2026-03-13-quantified-boolean-formulas-model.md +++ /dev/null @@ -1,99 +0,0 @@ -# Plan: Add QuantifiedBooleanFormulas Model - -**Issue:** #571 — [Model] QuantifiedBooleanFormulas(qbf)(*) -**Skill:** add-model - -## Information Checklist - -| # | Item | Value | -|---|------|-------| -| 1 | Problem name | `QuantifiedBooleanFormulas` | -| 2 | Mathematical definition | Given a fully quantified Boolean formula F=(Q_1 u_1)...(Q_n u_n)E where each Q_i is ∀ or ∃ and E is a CNF formula, determine whether F is true | -| 3 | Problem type | Satisfaction (Metric = bool) | -| 4 | Type parameters | None | -| 5 | Struct fields | `num_vars: usize`, `quantifiers: Vec`, `clauses: Vec` | -| 6 | Configuration space | `vec![2; num_vars]` — each variable is 0 or 1 | -| 7 | Feasibility check | A config represents a full assignment; evaluate returns true iff the formula is true under that assignment (ignoring quantifier semantics in evaluate — quantifier semantics are captured by the brute-force solver's game-tree search) | -| 8 | Objective function | `bool` — satisfied or not under the given assignment | -| 9 | Best known exact algorithm | O(2^n) brute-force game-tree evaluation (Stockmeyer & Meyer, 1973); complexity string: `"2^num_vars"` | -| 10 | Solving strategy | BruteForce works — but needs special handling: `find_satisfying` must find a *witnessing assignment* for the existential variables such that for all universal variable assignments, E is satisfied. The `evaluate()` method just checks if a single full assignment satisfies the CNF matrix E (standard SAT-like evaluation). | -| 11 | Category | `formula` | - -## Design Decisions - -### evaluate() Semantics -Following the check-issue comment's analysis, `evaluate()` will treat the config as a full assignment and check whether the CNF matrix E is satisfied. This is consistent with how the `Problem` trait works (a single config → metric). The quantifier semantics are implicit: a QBF is TRUE iff there exists an assignment to existential variables such that for ALL universal variable assignments, E evaluates to true. The brute-force solver enumerates all 2^n assignments and returns any satisfying one. - -### Quantifier Enum -Define a `Quantifier` enum with `Exists` and `ForAll` variants, serializable with serde. - -### Reusing CNFClause -Reuse the existing `CNFClause` type from `sat.rs` (1-indexed signed integers). - -## Steps - -### Step 1: Implement the model (`src/models/formula/qbf.rs`) - -1. Define `Quantifier` enum: `{ Exists, ForAll }` with `Debug, Clone, PartialEq, Eq, Serialize, Deserialize` -2. Define `QuantifiedBooleanFormulas` struct with fields: `num_vars`, `quantifiers`, `clauses` -3. Add `inventory::submit!` for `ProblemSchemaEntry` -4. Constructor: `new(num_vars, quantifiers, clauses)` with assertion that `quantifiers.len() == num_vars` -5. Getter methods: `num_vars()`, `num_clauses()`, `quantifiers()`, `clauses()` -6. Implement `Problem` trait: - - `NAME = "QuantifiedBooleanFormulas"` - - `Metric = bool` - - `dims() = vec![2; num_vars]` - - `evaluate(config)` — convert to bool assignment, check if all clauses are satisfied (same as SAT) - - `variant() = variant_params![]` -7. Implement `SatisfactionProblem` (marker trait) -8. Add `declare_variants!` with complexity `"2^num_vars"` -9. Add `is_true(&self) -> bool` method that implements proper QBF game-tree evaluation (recursive minimax) -10. Link test file: `#[cfg(test)] #[path = "../../unit_tests/models/formula/qbf.rs"] mod tests;` - -### Step 2: Register the model - -1. `src/models/formula/mod.rs` — add `pub(crate) mod qbf;` and `pub use qbf::{QuantifiedBooleanFormulas, Quantifier};` -2. `src/models/mod.rs` — add `QuantifiedBooleanFormulas, Quantifier` to the formula re-export line -3. `src/lib.rs` prelude — add `QuantifiedBooleanFormulas` to the formula prelude exports - -### Step 3: Register in CLI - -1. `problemreductions-cli/src/dispatch.rs`: - - Add import for `QuantifiedBooleanFormulas` - - Add `"QuantifiedBooleanFormulas" => deser_sat::(data)` in `load_problem()` - - Add `"QuantifiedBooleanFormulas" => try_ser::(any)` in `serialize_any_problem()` -2. `problemreductions-cli/src/problem_name.rs`: - - Add `"qbf" | "quantifiedbooleanformulas" => "QuantifiedBooleanFormulas".to_string()` in `resolve_alias()` - - Add `("QBF", "QuantifiedBooleanFormulas")` to `ALIASES` array - -### Step 4: Add CLI creation support - -1. `problemreductions-cli/src/commands/create.rs`: - - Add `"QuantifiedBooleanFormulas"` match arm: parse `--num-vars`, `--clauses`, and a new `--quantifiers` flag - - Add to `example_for()`: `"QuantifiedBooleanFormulas" => "--num-vars 3 --clauses \"1,2;-1,3\" --quantifiers \"E,A,E\""` -2. `problemreductions-cli/src/cli.rs`: - - Add `--quantifiers` flag to `CreateArgs`: `pub quantifiers: Option` - - Update `all_data_flags_empty()` to include `args.quantifiers.is_none()` - - Add QBF to "Flags by problem type" table - -### Step 5: Write unit tests (`src/unit_tests/models/formula/qbf.rs`) - -1. `test_quantifier_creation` — verify Quantifier enum -2. `test_qbf_creation` — construct instance, verify dimensions -3. `test_qbf_evaluate` — verify evaluate() on valid/invalid assignments -4. `test_qbf_is_true` — verify game-tree evaluation for known true/false instances -5. `test_qbf_solver` — verify brute-force solver finds satisfying assignments -6. `test_qbf_serialization` — round-trip serde test -7. `test_qbf_trivial` — empty formula, all-exists (reduces to SAT) - -### Step 6: Document in paper - -Add problem-def entry in `docs/paper/reductions.typ`: -- Add display name: `"QuantifiedBooleanFormulas": [Quantified Boolean Formulas (QBF)]` -- Add `#problem-def("QuantifiedBooleanFormulas")[...]` with formal definition and background - -### Step 7: Verify - -```bash -make test clippy fmt-check -``` diff --git a/docs/plans/2026-03-22-counting-problem-trait-design.md b/docs/plans/2026-03-22-counting-problem-trait-design.md deleted file mode 100644 index 5aa94a2d3..000000000 --- a/docs/plans/2026-03-22-counting-problem-trait-design.md +++ /dev/null @@ -1,244 +0,0 @@ -# CountingProblem Trait — Supporting #P and PP-Complete Problems - -**Date:** 2026-03-22 -**Status:** Approved design, pending implementation - -## Problem - -The current trait hierarchy supports two problem families: - -- `OptimizationProblem` (`Metric = SolutionSize`) — find a config that maximizes/minimizes an objective -- `SatisfactionProblem` (`Metric = bool`) — find a config satisfying all constraints - -8 issues are blocked because they model problems where the answer depends on **aggregating over the entire configuration space** — counting feasible configs or summing weighted probabilities. These are #P-complete or PP-complete problems (not known to be in NP) that don't fit either existing trait. - -### Blocked issues - -**Models:** #235 NetworkReliability, #237 NetworkSurvivability, #404 KthLargestSubset, #405 KthLargestMTuple - -**Rules (blocked on models above):** #256 SteinerTree → NetworkReliability, #257 VertexCover → NetworkSurvivability, #394 SubsetSum → KthLargestSubset, #395 SubsetSum → KthLargestMTuple - -## Design - -### New type: `Weight` - -A newtype wrapper for per-configuration weights, parallel to `SolutionSize` for optimization problems. Infeasible configs have weight zero — no separate `Infeasible` variant needed (unlike `SolutionSize::Invalid`) because a zero-weight config contributes nothing to the sum. - -```rust -// src/types.rs -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Weight(pub W); -``` - -### New trait: `CountingProblem` - -A marker trait parallel to `SatisfactionProblem`, binding `Metric = Weight`: - -```rust -// src/traits.rs -pub trait CountingProblem: Problem> { - /// The inner weight type (e.g., `u64` for unweighted counting, `f64` for probabilities). - type Value: Clone + AddAssign + Zero + PartialOrd + fmt::Debug + Serialize + DeserializeOwned; -} -``` - -The `evaluate(config) -> Weight` method (inherited from `Problem`) returns the weight of a single configuration. The "answer" to the problem is the sum of weights over all configurations. This is computed by the solver, not by `evaluate`. - -### Trait hierarchy (updated) - -``` -Problem (Metric: Clone) -├── OptimizationProblem (Metric = SolutionSize) — existing, unchanged -├── SatisfactionProblem (Metric = bool) — existing, unchanged -└── CountingProblem (Metric = Weight) — NEW -``` - -### Solver extension - -Add a separate `CountingSolver` trait (parallel to how problem families have distinct traits) rather than extending the existing `Solver` trait. This avoids forcing `ILPSolver` to implement a meaningless `count` method: - -```rust -// src/solvers/mod.rs (existing Solver trait unchanged) - -/// Solver trait for counting problems. -pub trait CountingSolver { - /// Compute the total weight (sum of evaluate over all configs). - fn count(&self, problem: &P) -> P::Value; -} - -// src/solvers/brute_force.rs -impl CountingSolver for BruteForce { - fn count(&self, problem: &P) -> P::Value { - let mut total = P::Value::zero(); - for config in DimsIterator::new(problem.dims()) { - total += problem.evaluate(&config).0; - } - total - } -} -``` - -`BruteForce` also gets a convenience method for testing: -```rust -/// Return all feasible configs and their weights alongside the total count. -pub fn count_with_configs(&self, problem: &P) - -> (P::Value, Vec<(Vec, P::Value)>); -``` - -### Reduction support - -Counting reductions preserve aggregate counts, not individual solutions. New traits parallel to `ReductionResult` / `ReduceTo`: - -```rust -// src/rules/traits.rs - -pub trait CountingReductionResult { - type Source: CountingProblem; - type Target: CountingProblem; - - /// Get a reference to the target problem. - fn target_problem(&self) -> &Self::Target; - - /// Transform the target's aggregate count back to the source's count. - /// - /// For parsimonious reductions (1-to-1 config mapping), this is identity. - /// For non-parsimonious reductions, this applies a correction factor - /// (e.g., divide by 2 if the reduction doubles feasible configs). - fn extract_count( - &self, - target_count: ::Value, - ) -> ::Value; -} - -pub trait ReduceToCount: CountingProblem { - type Result: CountingReductionResult; - fn reduce_to_count(&self) -> Self::Result; -} -``` - -### Registry and CLI integration - -#### `declare_variants!` macro - -Gets a new `count` keyword. The macro generates a `CountSolveFn` (instead of `SolveFn`) that calls `BruteForce::count()` and formats the result: - -```rust -crate::declare_variants! { - default count NetworkReliability => "2^num_edges * num_vertices", -} -``` - -The `count` keyword generates: -- A new `SolverKind::Count` variant in the proc macro's internal `SolverKind` enum (alongside existing `Opt` and `Sat`) -- A `solver_kind` field on `VariantEntry` to distinguish problem families at runtime (enum with `Optimization`, `Satisfaction`, `Counting` variants) -- A `count_fn: Option` field on `VariantEntry` where `CountSolveFn = fn(&dyn Any) -> String` -- The generated function downcasts `&dyn Any` to the concrete type, calls `BruteForce.count(&problem)`, and formats the result -- The existing `ProblemType` struct (which holds problem metadata, not a classification enum) is unchanged - -#### `LoadedDynProblem` - -Gets a new method: -```rust -pub fn solve_counting(&self) -> Option { - (self.count_fn?)(self.inner.as_any()) -} -``` - -The existing `solve_brute_force` remains unchanged for opt/sat problems. - -#### `pred solve` CLI - -The solve command checks `VariantEntry::solver_kind` to determine the dispatch path: -- `SolverKind::Optimization` / `SolverKind::Satisfaction` → existing `solve_brute_force()` -- `SolverKind::Counting` → new `solve_counting()`, displays `Total weight: ` - -#### `#[reduction]` proc macro - -The existing macro hardcodes `ReduceTo` trait detection. It must be extended to also recognize `ReduceToCount`: - -- When the macro sees `impl ReduceToCount for Source`, it generates a `reduce_count_fn` field on `ReductionEntry` -- The generated function returns a `Box` (new type-erased trait for counting reductions) -- `overhead` attribute works identically — overhead expressions are about problem size, not about solution type - -#### `ReductionEntry` changes - -`ReductionEntry` (in `src/rules/registry.rs`) gains new optional fields for counting reductions. A given entry has either `reduce_fn` (opt/sat) or `reduce_count_fn` (counting), never both: - -```rust -pub struct ReductionEntry { - // ... existing fields unchanged ... - pub reduce_fn: Option, // existing: opt/sat reductions - pub reduce_count_fn: Option, // NEW: counting reductions -} -``` - -Where `CountReduceFn = fn(&dyn Any) -> Box`. - -#### Reduction graph integration - -Counting edges and opt/sat edges coexist in the same `ReductionGraph`. The graph is about problem reachability — edge type doesn't affect pathfinding. The distinction matters only at solve time: - -- `ReductionEdgeData` gains an `edge_kind: EdgeKind` field (`enum EdgeKind { Standard, Counting }`) -- `reduce_along_path` checks edge kinds: a path must be homogeneous (all-standard or all-counting); mixed paths are invalid -- For all-counting paths, the runtime builds a `CountingReductionChain` instead of a `ReductionChain` - -#### Counting reduction chains - -For multi-hop counting paths (A →count→ B →count→ C): - -```rust -pub trait DynCountingReductionResult { - fn target_problem_any(&self) -> &dyn Any; - /// Transform target count to source count using serde_json::Value for type erasure. - fn extract_count_dyn(&self, target_count: serde_json::Value) -> serde_json::Value; -} -``` - -`CountingReductionChain` composes these: reduce A→B→C, solve C to get count as `serde_json::Value`, then call `extract_count_dyn` backwards through the chain. This parallels `ReductionChain` for opt/sat reductions. - -**Note on cross-type reductions:** When source and target have different `Value` types (e.g., `u64` → `f64`), the `extract_count` implementation is responsible for the type conversion. The `serde_json::Value` type erasure in `DynCountingReductionResult` handles this naturally at the runtime dispatch level. - -#### Exports - -Add to prelude and `lib.rs`: -- `CountingProblem`, `Weight`, `CountingSolver` traits/types -- `ReduceToCount`, `CountingReductionResult` traits - -### Concrete models - -All models store **only the counting problem data** — no decision thresholds (`k`, `q`). The threshold is part of the GJ decision formulation but not part of the counting problem we model. - -| Model | Value type | Fields | evaluate returns | -|---|---|---|---| -| `NetworkReliability` | `f64` | `graph`, `terminals`, `failure_probs` | `Weight(Π p_e^{x_e} · (1-p_e)^{1-x_e})` if terminals connected, else `Weight(0.0)` | -| `NetworkSurvivability` | `f64` | `graph`, `terminals`, `failure_probs` | Same pattern for survivability | -| `KthLargestSubset` | `u64` | `sizes`, `bound` | `Weight(1)` if subset sum ≤ bound, else `Weight(0)` | -| `KthLargestMTuple` | `u64` | `sizes`, `bound` | `Weight(1)` if m-tuple condition met, else `Weight(0)` | - -### `Weight` utility impls - -For ergonomics, `Weight` implements: -- `PartialOrd` where `W: PartialOrd` — delegates to inner value -- `Eq` where `W: Eq`, `Hash` where `W: Hash` — conditional impls (works for `u64`, not `f64`) -- `Add>` and `std::iter::Sum` where `W: Add` — enables `configs.map(evaluate).sum()` -- `Display` where `W: Display` — prints the inner value directly (e.g., `0.9832` not `Weight(0.9832)`) - -### What is NOT changed - -- `OptimizationProblem`, `SatisfactionProblem` — untouched -- `ReduceTo`, `ReductionResult` — untouched -- All existing models and rules — untouched -- Existing `Solver` trait — untouched (new `CountingSolver` is separate) - -## Alternatives considered - -1. **Generalized metric aggregation** (#737) — replace all three leaf traits with a single `Aggregation` enum. Elegant but large breaking refactor with no immediate payoff. Filed for future consideration. - -2. **Two-level trait** (`is_feasible` + `weight` methods) — more explicit but adds unnecessary surface area and boilerplate for unweighted counting. - -3. **`Metric = f64` without wrapper** — works but loses type safety. `Weight` follows the `SolutionSize` pattern and makes intent explicit. - -## Related issues - -- #737 — Generalized metric aggregation (future architecture) -- #748 — DefaultSolver per problem (future architecture) diff --git a/docs/plans/2026-03-22-generalized-aggregation-design.md b/docs/plans/2026-03-22-generalized-aggregation-design.md new file mode 100644 index 000000000..f467d20b3 --- /dev/null +++ b/docs/plans/2026-03-22-generalized-aggregation-design.md @@ -0,0 +1,477 @@ +# Generalized Aggregation -- Unified Problem Trait Hierarchy + +**Date:** 2026-03-22 +**Status:** Revised design, approved for implementation planning +**Supersedes:** `2026-03-22-counting-problem-trait-design.md` + +## Problem + +The current trait hierarchy hard-codes two witness-oriented problem families: + +- `OptimizationProblem` (`Metric = SolutionSize`, plus `direction()`) +- `SatisfactionProblem` (`Metric = bool`) + +That works for "find one config" workflows, but it does not scale to `#P` and probability problems where the answer is an aggregate over the whole configuration space. Adding a third parallel leaf trait for counting would unblock the immediate issues, but it would also duplicate the same branching in solvers, macros, registry dispatch, and reduction execution. + +The goal of this design is to unify value aggregation while preserving the existing witness-oriented workflows that the repo already depends on: + +- brute-force witness search +- solution extraction through reduction chains +- `pred reduce` bundles +- `pred solve bundle.json` +- ILP solve-via-reduction + +## Core idea + +Unify the **value layer**, not the **witness layer**. + +Each problem exposes a single aggregate value type. Solvers always know how to compute the final value by folding over all configurations. Some aggregate types also support recovering representative witness configurations; others do not. + +This keeps the mathematical core small while making the runtime honest about which operations are valid. + +## `Aggregate` trait + +`Aggregate` remains a monoid at its core, but it also exposes optional witness hooks with safe defaults. That is the minimal extra surface needed to keep dynamic witness APIs working without re-introducing a full parallel trait hierarchy. + +```rust +// src/types.rs +pub trait Aggregate: Clone + fmt::Debug + Serialize + DeserializeOwned { + /// Neutral element for folding over the configuration space. + fn identity() -> Self; + + /// Associative combine operation. + fn combine(self, other: Self) -> Self; + + /// Whether this aggregate admits representative witness configurations. + fn supports_witnesses() -> bool { + false + } + + /// Whether a per-configuration value belongs to the witness set + /// for the final aggregate value. + fn contributes_to_witnesses(_config_value: &Self, _total: &Self) -> bool { + false + } +} +``` + +The default witness behavior is deliberately conservative: + +- `Sum` and `And` remain value-only +- `Max`, `Min`, and `Or` opt in to witness recovery + +## Aggregate types + +Five concrete aggregate wrappers replace the current leaf-trait split: + +```rust +// src/types.rs +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Max(pub Option); + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Min(pub Option); + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Sum(pub W); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Or(pub bool); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct And(pub bool); +``` + +| Type | Identity | Combine | Witness support | Replaces | +|------|----------|---------|-----------------|----------| +| `Max` | `Max(None)` | keep larger `Some` | yes | `SolutionSize` + `Direction::Maximize` | +| `Min` | `Min(None)` | keep smaller `Some` | yes | `SolutionSize` + `Direction::Minimize` | +| `Sum` | `Sum(W::zero())` | numeric addition | no | counting / probability totals | +| `Or` | `Or(false)` | logical or | yes | `bool` existential problems | +| `And` | `And(true)` | logical and | no | universal / tautology-style problems | + +Witness semantics: + +- `Max` / `Min`: a config is a witness iff its aggregate value equals the final optimum and is feasible +- `Or`: a config is a witness iff it evaluates to `Or(true)` and the final total is `Or(true)` +- `Sum` / `And`: no single config is a representative witness, so witness APIs return `None` / empty + +## Unified `Problem` trait + +```rust +// src/traits.rs +pub trait Problem: Clone { + const NAME: &'static str; + type Value: Aggregate; + + fn dims(&self) -> Vec; + fn evaluate(&self, config: &[usize]) -> Self::Value; + + fn num_variables(&self) -> usize { + self.dims().len() + } + + fn variant() -> Vec<(&'static str, &'static str)>; + + fn problem_type() -> crate::registry::ProblemType { + crate::registry::find_problem_type(Self::NAME) + .unwrap_or_else(|| panic!("no catalog entry for Problem::NAME = {:?}", Self::NAME)) + } +} +``` + +Removed: + +- `OptimizationProblem` +- `SatisfactionProblem` +- `type Metric` +- `SolutionSize` +- `Direction` + +Unchanged: + +- `DeclaredVariant` +- `Problem::NAME` +- `dims()` +- `variant()` +- catalog bridge via `problem_type()` + +## Solvers + +### Value solving + +All problems support value solving through one fold: + +```rust +// src/solvers/mod.rs +pub trait Solver { + fn solve(&self, problem: &P) -> P::Value; +} +``` + +```rust +// src/solvers/brute_force.rs +impl Solver for BruteForce { + fn solve(&self, problem: &P) -> P::Value { + DimsIterator::new(problem.dims()) + .map(|config| problem.evaluate(&config)) + .fold(P::Value::identity(), P::Value::combine) + } +} +``` + +### Witness solving + +Witness APIs remain available, but only when the aggregate type opts in through the default hooks above: + +```rust +impl BruteForce { + pub fn find_witness(&self, problem: &P) -> Option>; + pub fn find_all_witnesses(&self, problem: &P) -> Vec>; + pub fn solve_with_witnesses(&self, problem: &P) + -> (P::Value, Vec>); +} +``` + +Behavior: + +- `Max` / `Min`: witnesses are the optimal configs +- `Or`: witnesses are satisfying configs +- `Sum` / `And`: `find_witness()` returns `None`, `find_all_witnesses()` returns `[]` + +This is the key distinction from the counting-only design: value aggregation is unified, but witness recovery is explicitly optional. + +## Dynamic solve surfaces + +The dynamic registry needs two solve entry points, not one: + +```rust +// src/registry/dyn_problem.rs +pub type SolveValueFn = fn(&dyn Any) -> String; +pub type SolveWitnessFn = fn(&dyn Any) -> Option<(Vec, String)>; +``` + +`VariantEntry` stores both: + +- `solve_value_fn` always exists +- `solve_witness_fn` always exists, but returns `None` for aggregate-only values (`Sum`, `And`) + +`LoadedDynProblem` mirrors that split: + +- `solve_brute_force_value() -> String` +- `solve_brute_force_witness() -> Option<(Vec, String)>` + +This keeps `declare_variants!` simple: + +- the `opt` / `sat` keywords disappear +- the generated value-solve closure always calls `Solver::solve()` +- the generated witness-solve closure always calls `BruteForce::find_witness()` + +No solver-kind branching is needed at variant registration time. + +## CLI behavior + +### `pred solve problem.json` + +Always computes the aggregate value. + +- If a witness exists, print both `Solution` and `Evaluation` +- If no witness exists, print only `Evaluation` + +Examples: + +- `Max(Some(42))` -> solution config + `Maximum: 42` +- `Or(true)` -> solution config + `Satisfiable: true` +- `Sum(0.9832)` -> no single solution config, print `Sum: 0.9832` +- `And(false)` -> no single solution config, print `Tautology: false` + +### `pred solve bundle.json` + +Remains a **witness-only** workflow in this design. + +Bundles exist to solve a target problem and map a target configuration back through `extract_solution`. That makes sense only for witness-capable problems and witness-capable reduction paths. + +If the target variant or the path is aggregate-only, bundle solving is rejected early with a clear error. + +### `--solver ilp` + +Also remains **witness-only**. + +ILP support in this repo is a witness-producing solve-via-reduction path. Aggregate-only problems (`Sum`, `And`) do not have an ILP mode unless a future design introduces a threshold or certificate-bearing witness formulation. + +The immediate design change is: + +- keep the ILP solver internals unchanged +- require witness-capable source problems +- require a witness-capable path from source to `ILP` + +## Reductions + +Two reduction traits remain necessary because config mapping and aggregate-value mapping are genuinely different operations. + +```rust +// src/rules/traits.rs +pub trait ReductionResult { + type Source: Problem; + type Target: Problem; + fn target_problem(&self) -> &Self::Target; + fn extract_solution(&self, target_solution: &[usize]) -> Vec; +} + +pub trait ReduceTo: Problem { + type Result: ReductionResult; + fn reduce_to(&self) -> Self::Result; +} + +pub trait AggregateReductionResult { + type Source: Problem; + type Target: Problem; + fn target_problem(&self) -> &Self::Target; + fn extract_value( + &self, + target_value: ::Value, + ) -> ::Value; +} + +pub trait ReduceToAggregate: Problem { + type Result: AggregateReductionResult; + fn reduce_to_aggregate(&self) -> Self::Result; +} +``` + +Type-erased runtime support likewise splits: + +- `DynReductionResult` for witness/config reductions +- `DynAggregateReductionResult` for aggregate/value reductions + +## `EdgeCapabilities` + +The reduction graph needs explicit edge-mode metadata so path search can reject incompatible paths before execution. + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct EdgeCapabilities { + pub witness: bool, + pub aggregate: bool, +} +``` + +Capability assignment: + +- `ReduceTo` edges -> `{ witness: true, aggregate: false }` +- `ReduceToAggregate` edges -> `{ witness: false, aggregate: true }` +- natural subtype / `ReductionAutoCast` edges -> `{ witness: true, aggregate: true }` + +Why the natural edges are both: + +- witness mode: the config mapping is identity +- aggregate mode: the value mapping is also identity because the problem semantics do not change + +## Mode-aware pathfinding + +Pathfinding stays on one graph, but it now receives a required capability: + +```rust +pub enum ReductionMode { + Witness, + Aggregate, +} +``` + +`ReductionGraph::find_cheapest_path(...)` becomes capability-aware: + +- witness callers traverse only edges with `capabilities.witness` +- aggregate callers traverse only edges with `capabilities.aggregate` + +This prevents "valid graph path, invalid runtime execution" failures. + +Mode usage: + +- `pred reduce` -> witness +- `pred solve bundle.json` -> witness +- ILP solve-via-reduction -> witness +- future aggregate chain execution -> aggregate +- graph export / inspection -> all edges, with capability metadata shown + +## Aggregate reduction chains + +Witness execution stays on `ReductionChain`. + +Aggregate execution gets its own chain: + +```rust +pub struct AggregateReductionChain { + steps: Vec>, +} +``` + +with: + +- `target_problem_any()` +- backwards composition of `extract_value_dyn(...)` + +The important point is that witness execution and aggregate execution are separate entry points over the same graph, selected by `ReductionMode`. + +## Registry and graph changes + +### `ReductionEntry` + +`ReductionEntry` gains: + +- `reduce_fn: Option` +- `reduce_aggregate_fn: Option` +- `capabilities: EdgeCapabilities` + +### `ReductionEdgeData` + +`ReductionEdgeData` gains: + +- `capabilities: EdgeCapabilities` +- optional witness executor +- optional aggregate executor + +### Graph export + +The JSON export includes: + +- `witness: bool` +- `aggregate: bool` + +instead of a single coarse edge-kind label. + +## Model migration examples + +### Optimization + +```rust +impl Problem for MaximumIndependentSet { + type Value = Max; + + fn evaluate(&self, config: &[usize]) -> Max { + if invalid { + Max(None) + } else { + Max(Some(size)) + } + } +} +``` + +### Satisfaction + +```rust +impl Problem for Satisfiability { + type Value = Or; + + fn evaluate(&self, config: &[usize]) -> Or { + Or(satisfies) + } +} +``` + +### Counting + +```rust +impl Problem for NetworkReliability { + type Value = Sum; + + fn evaluate(&self, config: &[usize]) -> Sum { + if terminals_connected { + Sum(probability_weight) + } else { + Sum(0.0) + } + } +} +``` + +## Migration scope + +| Area | Change | +|------|--------| +| `src/types.rs` | replace `SolutionSize` / `Direction` with aggregate wrappers and witness hooks | +| `src/traits.rs` | unify on `Problem` | +| `src/solvers/` | one value fold plus generic witness helpers | +| `src/registry/` | split value solve from witness solve | +| `problemreductions-macros/` | remove `opt` / `sat`, emit both dynamic solve closures | +| `src/rules/` | add aggregate reductions and capability-aware path execution | +| `problemreductions-cli/` | differentiate value-only vs witness-capable solve output | +| existing model/test files | mechanical `Metric -> Value` migration | + +## What is not changed + +- problem names, aliases, and variant resolution +- the overall CLI command set +- the catalog bridge via `ProblemType` +- the fact that ILP is a witness-oriented backend +- the paper format in `docs/paper/reductions.typ` + +## Deferred follow-up work + +Out of scope for this design revision: + +- threshold-specific decision wrappers for `Sum` problems +- a new aggregate-only bundle format +- universal counterexample extraction for `And` +- choosing default reduction modes in graph-inspection UX + +## Alternatives considered + +1. **Minimal `CountingProblem` extension** + - Lowest short-term diff + - Repeats the branching in solvers, registry dispatch, macros, and reductions + +2. **Unify value aggregation but keep witness-oriented runtime explicit** (chosen) + - Solves the architectural duplication + - Preserves the witness assumptions already embedded in the repo + +3. **Single edge kind with runtime rejection** + - Smaller patch + - Bad UX and bad API: pathfinding would still return paths that cannot be executed + +## Related issues + +- #737 -- original aggregation architecture issue +- #748 -- default solver per problem (future, orthogonal) +- #235, #237, #404, #405 -- counting models enabled by this refactor +- #256, #257, #394, #395 -- aggregate-value reductions enabled by this refactor diff --git a/docs/src/cli.md b/docs/src/cli.md index 452532cf9..129ec7c52 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -85,7 +85,7 @@ pred solve problem.json --solver brute-force # LengthBoundedDisjointPaths currently needs brute-force pred solve lbdp.json --solver brute-force -# Evaluate a specific configuration (shows Valid(N) or Invalid) +# Evaluate a specific configuration (shows the aggregate value, e.g. Max(2) or Min(None)) pred evaluate problem.json --config 1,0,1,0 # Reduce to another problem type and solve via brute-force @@ -442,7 +442,7 @@ Evaluate a configuration against a problem instance: ```bash $ pred evaluate problem.json --config 1,0,1,0 -Valid(2) +Max(2) ``` Stdin is supported with `-`: @@ -527,7 +527,7 @@ $ pred solve problem.json Problem: MaximumIndependentSet (reduced to ILP) Solver: ilp Solution: [1, 0, 0, 1] -Evaluation: Valid(2) +Evaluation: Max(2) ``` Solve a reduction bundle (from `pred reduce`): @@ -537,9 +537,9 @@ $ pred solve reduced.json --solver brute-force Source: MaximumIndependentSet Target: QUBO (solved with brute-force) Target solution: [0, 1, 0, 1] -Target evaluation: Valid(-2.0) +Target evaluation: Min(-2.0) Source solution: [0, 1, 0, 1] -Source evaluation: Valid(2) +Source evaluation: Max(2) ``` > **Note:** The ILP solver requires a reduction path from the target problem to ILP. diff --git a/docs/src/design.md b/docs/src/design.md index 3fff7a7cb..7f709edfc 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -25,29 +25,25 @@ This guide covers the library internals for contributors. ## Problem Model -Every problem implements `Problem`. Optimization problems additionally implement `OptimizationProblem`; satisfaction problems implement `SatisfactionProblem`. +Every problem implements `Problem`. The associated `Value` type is the per-configuration aggregate returned by `evaluate()`. Solvers fold these values across the configuration space, and witness-capable aggregates can also recover representative configurations. ```rust,ignore trait Problem: Clone { const NAME: &'static str; // e.g., "MaximumIndependentSet" - type Metric: Clone; // SolutionSize or bool + type Value: Clone; // e.g., Max, Or, Sum fn dims(&self) -> Vec; // config space per variable - fn evaluate(&self, config: &[usize]) -> Self::Metric; + fn evaluate(&self, config: &[usize]) -> Self::Value; fn variant() -> Vec<(&'static str, &'static str)>; // e.g., [("graph", "SimpleGraph"), ("weight", "i32")] fn num_variables(&self) -> usize; // default: dims().len() + fn problem_type() -> ProblemType; // default: registry lookup by NAME } - -trait OptimizationProblem: Problem> { - type Value: PartialOrd + Clone; // e.g., i32, f64 - fn direction(&self) -> Direction; // Maximize or Minimize -} - -trait SatisfactionProblem: Problem {} // marker trait ``` -- **`Problem`** — the base trait. Every problem declares a `NAME` (e.g., `"MaximumIndependentSet"`). The solver explores the configuration space defined by `dims()` and scores each configuration with `evaluate()`. For example, a 4-vertex MIS has `dims() = [2, 2, 2, 2]` (each vertex is selected or not); `evaluate(&[1, 0, 1, 0])` returns `Valid(2)` if vertices 0 and 2 form an independent set, or `Invalid` if they share an edge. Each problem also provides inherent getter methods (e.g., `num_vertices()`, `num_edges()`) used by reduction overhead expressions. -- **`OptimizationProblem`** — extends `Problem` with a comparable `Value` type and a `direction()` (`Maximize` or `Minimize`). -- **`SatisfactionProblem`** — constrains `Metric = bool`: `true` if all constraints are satisfied, `false` otherwise. +- **`Problem`** — the base trait. Every problem declares a `NAME` (e.g., `"MaximumIndependentSet"`). The solver explores the configuration space defined by `dims()` and scores each configuration with `evaluate()`. For example, a 4-vertex MIS has `dims() = [2, 2, 2, 2]` (each vertex is selected or not); `evaluate(&[1, 0, 1, 0])` returns `Max(Some(2))` if vertices 0 and 2 form an independent set, or `Max(None)` if they share an edge. Each problem also provides inherent getter methods (e.g., `num_vertices()`, `num_edges()`) used by reduction overhead expressions. +- **Witness-capable objective problems** — typically use `Max`, `Min`, or `Extremum` as `Value`. +- **Witness-capable feasibility problems** — typically use `Or`. +- **Aggregate-only problems** — use fold values such as `Sum` or `And`; these solve to a value but do not admit representative witness configurations. +- **Common aggregate wrappers** — `Max`, `Min`, `Sum`, `Or`, `And`, `Extremum`, `ExtremumSense`. ## Variant System @@ -315,15 +311,17 @@ Solvers implement the `Solver` trait: ```rust,ignore pub trait Solver { - fn find_best(&self, problem: &P) -> Option>; - fn find_satisfying>(&self, problem: &P) -> Option>; + fn solve

(&self, problem: &P) -> P::Value + where + P: Problem, + P::Value: Aggregate; } ``` | Solver | Description | |--------|-------------| -| **BruteForce** | Enumerates all configurations. Also provides `find_all_best()` and `find_all_satisfying()`. Used for testing and verification. | -| **ILPSolver** | Enabled by default (`ilp` feature). Uses HiGHS via `good_lp`. Also provides `solve_reduced()` for problems that implement `ReduceTo`. | +| **BruteForce** | Enumerates all configurations. `solve()` works for any aggregate problem; `find_witness()`, `find_all_witnesses()`, and `solve_with_witnesses()` are available when `P::Value` supports witnesses. Used for testing and verification. | +| **ILPSolver** | Enabled by default. Solves ILP instances directly with HiGHS via `good_lp`. Also provides `solve_reduced()` for witness-capable problems that implement `ReduceTo>`. | ## JSON Serialization diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md index 2cae2206a..39d691ebf 100644 --- a/docs/src/getting-started.md +++ b/docs/src/getting-started.md @@ -91,12 +91,12 @@ configuration space. ```rust,ignore let solution = reduction.extract_solution(&ilp_solution); let metric = problem.evaluate(&solution); -println!("Packing solution: {:?} -> size {:?}", solution, metric); +println!("Packing solution: {:?} -> size {}", solution, metric); assert!(metric.is_valid()); ``` ```text -Packing solution: [1, 0, 1, 1] -> size Valid(3) +Packing solution: [1, 0, 1, 1] -> size Max(3) ``` For convenience, `ILPSolver::solve_reduced` combines reduce + solve + extract diff --git a/docs/src/static/reduction-workflow-dark.svg b/docs/src/static/reduction-workflow-dark.svg index b04e1c2cc..8e97a4ad2 100644 --- a/docs/src/static/reduction-workflow-dark.svg +++ b/docs/src/static/reduction-workflow-dark.svg @@ -38,10 +38,10 @@ - + - + @@ -53,10 +53,13 @@ - - - - + + + + + + + @@ -115,29 +118,29 @@ - - - - - - - - + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -158,29 +161,29 @@ - - - - - - - - + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -201,29 +204,29 @@ - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + @@ -244,32 +247,32 @@ - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -318,8 +321,8 @@ - - + + @@ -333,94 +336,94 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/docs/src/static/reduction-workflow.svg b/docs/src/static/reduction-workflow.svg index 1254938ab..334c2c182 100644 --- a/docs/src/static/reduction-workflow.svg +++ b/docs/src/static/reduction-workflow.svg @@ -38,10 +38,10 @@ - + - + @@ -53,10 +53,13 @@ - - - - + + + + + + + @@ -115,29 +118,29 @@ - - - - - - - - + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -158,29 +161,29 @@ - - - - - - - - + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -201,29 +204,29 @@ - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + @@ -244,32 +247,32 @@ - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -318,8 +321,8 @@ - - + + @@ -333,94 +336,94 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/docs/src/static/reduction-workflow.typ b/docs/src/static/reduction-workflow.typ index cbb5358ff..df911fb8b 100644 --- a/docs/src/static/reduction-workflow.typ +++ b/docs/src/static/reduction-workflow.typ @@ -27,7 +27,7 @@ // Edges with labels edge(, , "->", stroke: 1.5pt + accent, label: text(size: 9pt)[`reduce_to`], label-sep: 5pt, label-pos: 0.5, label-side: left), - edge(, , "->", stroke: 1.5pt + accent, label: text(size: 9pt)[`find_best`], label-sep: 5pt, label-pos: 0.5, label-side: left), + edge(, , "->", stroke: 1.5pt + accent, label: text(size: 9pt)[`find_witness`], label-sep: 5pt, label-pos: 0.5, label-side: left), edge(, , "->", stroke: 1.5pt + success, label: text(size: 9pt)[`extract_solution`], label-sep: 2pt, label-pos: 0.5, label-side: left), ) } diff --git a/docs/src/static/trait-hierarchy-dark.svg b/docs/src/static/trait-hierarchy-dark.svg index e6df22f04..e932783a4 100644 --- a/docs/src/static/trait-hierarchy-dark.svg +++ b/docs/src/static/trait-hierarchy-dark.svg @@ -1,29 +1,43 @@ - + - - - - + + + + - + - - - - - - - + + + + + + + + + + + + + + + + + + + + + @@ -33,26 +47,29 @@ - - - - + + + + - + - - - - - - - + + + + + + + + + + @@ -62,29 +79,29 @@ - - + + - + - - - - - - - - - - - - + + + + + + + + + + + + @@ -107,18 +124,17 @@ - - - - - - - - - - - - + + + + + + + + + + + @@ -164,12 +180,11 @@ - - - - - - + + + + + @@ -210,202 +225,143 @@ - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -415,77 +371,99 @@ - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -501,52 +479,94 @@ - + + + + + + + + + + + + + - - + + - - + + + + + - + + + + - + - - + + + + + + + + + + + + + + - + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + @@ -591,24 +611,18 @@ - - - - - - - - - + + + @@ -624,113 +638,119 @@ - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + - - + + - - + + - - + + - - + + + + + - - + + + + + - - + + + + + + + + + + + - - - - - + + - - + + - - + + @@ -738,92 +758,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/docs/src/static/trait-hierarchy.svg b/docs/src/static/trait-hierarchy.svg index e1e9bc3a8..571ca9c90 100644 --- a/docs/src/static/trait-hierarchy.svg +++ b/docs/src/static/trait-hierarchy.svg @@ -1,29 +1,43 @@ - + - - - - + + + + - + - - - - - - - + + + + + + + + + + + + + + + + + + + + + @@ -33,26 +47,29 @@ - - - - + + + + - + - - - - - - - + + + + + + + + + + @@ -62,29 +79,29 @@ - - + + - + - - - - - - - - - - - - + + + + + + + + + + + + @@ -107,18 +124,17 @@ - - - - - - - - - - - - + + + + + + + + + + + @@ -164,12 +180,11 @@ - - - - - - + + + + + @@ -210,202 +225,143 @@ - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -415,77 +371,99 @@ - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -501,52 +479,94 @@ - + + + + + + + + + + + + + - - + + - - + + + + + - + + + + - + - - + + + + + + + + + + + + + + - + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + @@ -591,24 +611,18 @@ - - - - - - - - - + + + @@ -624,113 +638,119 @@ - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + - - + + - - + + - - + + - - + + + + + - - + + + + + - - + + + + + + + + + + + - - - - - + + - - + + - - + + @@ -738,92 +758,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/docs/src/static/trait-hierarchy.typ b/docs/src/static/trait-hierarchy.typ index 74e6691e2..c23dd50dc 100644 --- a/docs/src/static/trait-hierarchy.typ +++ b/docs/src/static/trait-hierarchy.typ @@ -24,49 +24,43 @@ spacing: (8mm, 12mm), // Problem trait (top center) - node((0.5, 0), box(width: 55mm, align(left)[ + node((0.6, 0), box(width: 55mm, align(left)[ #strong[trait Problem]\ #text(size: 8pt, fill: secondary)[ `const NAME: &str`\ - `type Metric: Clone`\ + `type Value: Clone`\ `fn dims() -> Vec`\ - `fn evaluate(&config) -> Metric`\ + `fn evaluate(&config) -> Value`\ `fn variant() -> Vec<(&str, &str)>` ] ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), - // OptimizationProblem trait (bottom left) + // Aggregate trait (bottom left) node((0, 1), box(width: 55mm, align(left)[ - #strong[trait OptimizationProblem]\ + #strong[trait Aggregate]\ #text(size: 8pt, fill: secondary)[ - `type Value: PartialOrd + Clone`\ - `fn direction() -> Direction`\ - #text(style: "italic")[requires `Metric = SolutionSize`] - - #strong[SolutionSize\]\ - #text(size: 8pt, fill: secondary)[`Valid(T) | Invalid`] - - #strong[Direction]\ - #text(size: 8pt, fill: secondary)[`Maximize | Minimize`] - + `fn identity() -> Self`\ + `fn combine(self, other) -> Self`\ + `fn supports_witnesses() -> bool`\ + `fn contributes_to_witnesses(...)` ] - ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), + ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), - // SatisfactionProblem trait (bottom right) - node((1.2, 1), box(width: 42mm, align(left)[ - #strong[trait SatisfactionProblem]\ + // Common value types (bottom right) + node((1.25, 1), box(width: 48mm, align(left)[ + #strong[Common Value Types]\ #text(size: 8pt, fill: secondary)[ - #text(style: "italic")[marker trait]\ - #text(style: "italic")[requires `Metric = bool`] + `Max | Min | Extremum`\ + `Or | Sum | And`\ + #text(style: "italic")[used as `Problem::Value`] ] - ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), + ]), fill: type-fill, corner-radius: 6pt, inset: 10pt, name: ), - // Inheritance arrows - edge(, , "->", label: text(size: 8pt)[extends], label-side: left, label-fill: none), - edge(, , "->", label: text(size: 8pt)[extends], label-side: right, label-fill: none), + // Conceptual relationships + edge(, , "->", label: text(size: 8pt)[solver-bound on `Value`], label-side: left, label-fill: none), + edge(, , "->", label: text(size: 8pt)[implements], label-side: right, label-fill: none), ) } #let standalone-dark = sys.inputs.at("dark", default: "false") == "true" #trait-hierarchy(dark: standalone-dark) - diff --git a/examples/export_module_graph.rs b/examples/export_module_graph.rs index 6245dc211..f1959d21a 100644 --- a/examples/export_module_graph.rs +++ b/examples/export_module_graph.rs @@ -116,28 +116,19 @@ fn main() { "trait", "Core trait for all computational problems", ), - ( - "OptimizationProblem", - "trait", - "Extension trait for optimization problems", - ), - ( - "SatisfactionProblem", - "trait", - "Marker trait for satisfaction problems", - ), ], ), ( "types", "core", &[ - ( - "SolutionSize", - "enum", - "Metric for optimization: Valid(T) or Invalid", - ), - ("Direction", "enum", "Maximize or Minimize"), + ("Aggregate", "trait", "Trait for aggregate value types"), + ("Max", "struct", "Maximum aggregate wrapper"), + ("Min", "struct", "Minimum aggregate wrapper"), + ("Sum", "struct", "Summation aggregate wrapper"), + ("Or", "struct", "Existential (logical or) aggregate"), + ("And", "struct", "Universal (logical and) aggregate"), + ("Extremum", "struct", "Runtime max/min aggregate"), ("One", "struct", "Unit weight marker type"), ("WeightElement", "trait", "Trait for weight types"), ], @@ -172,13 +163,23 @@ fn main() { ( "ReduceTo", "trait", - "Trait for reducing one problem to another", + "Trait for witness/config reductions", ), ( "ReductionResult", "trait", "Result of a reduction with solution extraction", ), + ( + "ReduceToAggregate", + "trait", + "Trait for aggregate/value reductions", + ), + ( + "AggregateReductionResult", + "trait", + "Result of a reduction with value extraction", + ), ], ), ( @@ -203,7 +204,7 @@ fn main() { ( "Solver", "trait", - "Solver trait for optimization and satisfaction", + "Solver trait for aggregate value computation", ), ], ), diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 9c65f9262..21183fb7d 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -931,25 +931,25 @@ fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> { eprintln!(" --{:<16} {} ({})", "arcs", field.description, hint); } else if field.type_name == "MixedGraph" { eprintln!( - " --{:<16} {} ({})", - "graph", "Undirected edges E of the mixed graph", "edge list: 0-1,1-2,2-3" + " --{:<16} Undirected edges E of the mixed graph (edge list: 0-1,1-2,2-3)", + "graph" ); eprintln!( - " --{:<16} {} ({})", - "arcs", "Directed arcs A of the mixed graph", "directed arcs: 0>1,1>2,2>0" + " --{:<16} Directed arcs A of the mixed graph (directed arcs: 0>1,1>2,2>0)", + "arcs" ); } else if field.type_name == "BipartiteGraph" { eprintln!( - " --{:<16} {} ({})", - "left", "Vertices in the left partition", "integer" + " --{:<16} Vertices in the left partition (integer)", + "left" ); eprintln!( - " --{:<16} {} ({})", - "right", "Vertices in the right partition", "integer" + " --{:<16} Vertices in the right partition (integer)", + "right" ); eprintln!( - " --{:<16} {} ({})", - "biedges", "Bipartite edges as left-right pairs", "edge list: 0-0,0-1,1-2" + " --{:<16} Bipartite edges as left-right pairs (edge list: 0-0,0-1,1-2)", + "biedges" ); } else { let hint = help_flag_hint(canonical, &field.name, &field.type_name, graph_type); @@ -6051,11 +6051,7 @@ fn create_random( let num_edges = graph.num_edges(); let edge_weights = vec![1i32; num_edges]; let source = 0; - let sink = if num_vertices > 1 { - num_vertices - 1 - } else { - 0 - }; + let sink = num_vertices.saturating_sub(1); let size_bound = num_vertices; // no effective size constraint let cut_bound = num_edges as i32; // generous bound let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]); diff --git a/problemreductions-cli/src/commands/graph.rs b/problemreductions-cli/src/commands/graph.rs index d409efaa4..e27caaaa1 100644 --- a/problemreductions-cli/src/commands/graph.rs +++ b/problemreductions-cli/src/commands/graph.rs @@ -2,7 +2,7 @@ use crate::output::OutputConfig; use crate::problem_name::{aliases_for, parse_problem_spec, resolve_problem_ref}; use anyhow::{Context, Result}; use problemreductions::registry::collect_schemas; -use problemreductions::rules::{Minimize, MinimizeSteps, ReductionGraph, TraversalDirection}; +use problemreductions::rules::{Minimize, MinimizeSteps, ReductionGraph, TraversalFlow}; use problemreductions::types::ProblemSize; use problemreductions::{big_o_normal_form, Expr}; use std::collections::BTreeMap; @@ -693,11 +693,11 @@ pub fn export(out: &OutputConfig) -> Result<()> { out.emit_with_default_name("reduction_graph.json", &text, &json) } -fn parse_direction(s: &str) -> Result { +fn parse_direction(s: &str) -> Result { match s { - "out" => Ok(TraversalDirection::Outgoing), - "in" => Ok(TraversalDirection::Incoming), - "both" => Ok(TraversalDirection::Both), + "out" => Ok(TraversalFlow::Outgoing), + "in" => Ok(TraversalFlow::Incoming), + "both" => Ok(TraversalFlow::Both), _ => anyhow::bail!("Unknown direction: {}. Use 'out', 'in', or 'both'.", s), } } @@ -718,9 +718,9 @@ pub fn neighbors( let neighbors = graph.k_neighbors(&spec_name, &variant, max_hops, direction); let dir_label = match direction { - TraversalDirection::Outgoing => "outgoing", - TraversalDirection::Incoming => "incoming", - TraversalDirection::Both => "both directions", + TraversalFlow::Outgoing => "outgoing", + TraversalFlow::Incoming => "incoming", + TraversalFlow::Both => "both directions", }; // Build tree structure via BFS with parent tracking diff --git a/problemreductions-cli/src/commands/reduce.rs b/problemreductions-cli/src/commands/reduce.rs index 2fc5c9be5..7e579ed67 100644 --- a/problemreductions-cli/src/commands/reduce.rs +++ b/problemreductions-cli/src/commands/reduce.rs @@ -5,7 +5,9 @@ use crate::dispatch::{ use crate::output::OutputConfig; use crate::problem_name::resolve_problem_ref; use anyhow::{Context, Result}; -use problemreductions::rules::{MinimizeSteps, ReductionGraph, ReductionPath, ReductionStep}; +use problemreductions::rules::{ + MinimizeSteps, ReductionGraph, ReductionMode, ReductionPath, ReductionStep, +}; use problemreductions::types::ProblemSize; use std::collections::BTreeMap; use std::path::Path; @@ -112,18 +114,19 @@ pub fn reduce( // Auto-discover cheapest path let input_size = ProblemSize::new(vec![]); - let best_path = graph.find_cheapest_path( + let best_path = graph.find_cheapest_path_mode( source_name, &source_variant, &dst_ref.name, &dst_ref.variant, + ReductionMode::Witness, &input_size, &MinimizeSteps, ); best_path.ok_or_else(|| { anyhow::anyhow!( - "No reduction path from {} to {}\n\n\ + "No witness-capable reduction path from {} to {}\n\n\ Hint: generate a path file first, then pass it with --via:\n\ pred path {} {} -o path.json\n\ pred reduce {} --via path.json -o reduced.json", @@ -139,7 +142,11 @@ pub fn reduce( // 4. Execute reduction chain via reduce_along_path let chain = graph .reduce_along_path(&reduction_path, source.as_any()) - .ok_or_else(|| anyhow::anyhow!("Failed to execute reduction chain"))?; + .ok_or_else(|| { + anyhow::anyhow!( + "Reduction bundles require witness-capable paths; this path cannot produce a recoverable witness." + ) + })?; // 5. Serialize target let target_step = reduction_path.steps.last().unwrap(); diff --git a/problemreductions-cli/src/commands/solve.rs b/problemreductions-cli/src/commands/solve.rs index 663ee33b3..1e467d43a 100644 --- a/problemreductions-cli/src/commands/solve.rs +++ b/problemreductions-cli/src/commands/solve.rs @@ -29,6 +29,42 @@ fn parse_input(path: &Path) -> Result { } } +fn solve_result_text(problem: &str, solver: &str, result: &crate::dispatch::SolveResult) -> String { + let mut text = format!("Problem: {}\nSolver: {}", problem, solver); + if let Some(config) = &result.config { + text.push_str(&format!("\nSolution: {:?}", config)); + } + text.push_str(&format!("\nEvaluation: {}", result.evaluation)); + text +} + +fn solve_result_json( + problem: &str, + solver: &str, + result: &crate::dispatch::SolveResult, +) -> serde_json::Value { + let mut json = serde_json::json!({ + "problem": problem, + "solver": solver, + "evaluation": result.evaluation, + }); + if let Some(config) = &result.config { + json["solution"] = serde_json::json!(config); + } + json +} + +fn plain_problem_output( + problem: &str, + solver: &str, + result: &crate::dispatch::SolveResult, +) -> (String, serde_json::Value) { + ( + solve_result_text(problem, solver, result), + solve_result_json(problem, solver, result), + ) +} + pub fn solve(input: &Path, solver_name: &str, timeout: u64, out: &OutputConfig) -> Result<()> { if solver_name != "brute-force" && solver_name != "ilp" { anyhow::bail!( @@ -79,17 +115,8 @@ fn solve_problem( match solver_name { "brute-force" => { - let result = problem.solve_brute_force()?; - let text = format!( - "Problem: {}\nSolver: brute-force\nSolution: {:?}\nEvaluation: {}", - name, result.config, result.evaluation, - ); - let json = serde_json::json!({ - "problem": name, - "solver": "brute-force", - "solution": result.config, - "evaluation": result.evaluation, - }); + let result = problem.solve_brute_force(); + let (text, json) = plain_problem_output(name, "brute-force", &result); let result = out.emit_with_default_name("", &text, &json); if out.output.is_none() && crate::output::stderr_is_tty() { out.info("\nHint: use -o to save full solution details as JSON."); @@ -103,16 +130,12 @@ fn solve_problem( } else { "ilp (via ILP)".to_string() }; - let text = format!( - "Problem: {}\nSolver: {}\nSolution: {:?}\nEvaluation: {}", - name, solver_desc, result.config, result.evaluation, - ); - let mut json = serde_json::json!({ - "problem": name, - "solver": "ilp", - "solution": result.config, - "evaluation": result.evaluation, - }); + let result = crate::dispatch::SolveResult { + config: Some(result.config), + evaluation: result.evaluation, + }; + let text = solve_result_text(name, &solver_desc, &result); + let mut json = solve_result_json(name, "ilp", &result); if name != "ILP" { json["reduced_to"] = serde_json::json!("ILP"); } @@ -138,7 +161,12 @@ fn solve_bundle(bundle: ReductionBundle, solver_name: &str, out: &OutputConfig) // 2. Solve the target problem let target_result = match solver_name { - "brute-force" => target.solve_brute_force()?, + "brute-force" => target.solve_brute_force_witness().ok_or_else(|| { + anyhow::anyhow!( + "Bundle solving requires a witness-capable target problem and witness-capable reduction path; {} only supports aggregate-value solving.", + target_name + ) + })?, "ilp" => target.solve_with_ilp().map_err(add_ilp_solver_hint)?, _ => unreachable!(), }; @@ -167,9 +195,9 @@ fn solve_bundle(bundle: ReductionBundle, solver_name: &str, out: &OutputConfig) let chain = graph .reduce_along_path(&reduction_path, source.as_any()) - .ok_or_else(|| { - anyhow::anyhow!("Failed to re-execute reduction chain for solution extraction") - })?; + .ok_or_else(|| anyhow::anyhow!( + "Bundle solving requires a witness-capable reduction path; this bundle cannot recover a source solution." + ))?; // 4. Extract solution back to source problem space let source_config = chain.extract_solution(&target_result.config); @@ -203,7 +231,9 @@ fn solve_bundle(bundle: ReductionBundle, solver_name: &str, out: &OutputConfig) fn add_ilp_solver_hint(err: anyhow::Error) -> anyhow::Error { let message = err.to_string(); - if message.starts_with("No reduction path from ") && message.ends_with(" to ILP") { + if (message.starts_with("No reduction path from ") && message.ends_with(" to ILP")) + || message.contains("witness-capable") + { anyhow::anyhow!( "{message}\n\nHint: try `--solver brute-force` for direct exhaustive search on small instances." ) @@ -211,3 +241,41 @@ fn add_ilp_solver_hint(err: anyhow::Error) -> anyhow::Error { err } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::dispatch::SolveResult; + use crate::output::OutputConfig; + use crate::test_support::aggregate_bundle; + + #[test] + fn test_solve_value_only_problem_omits_solution() { + let result = SolveResult { + config: None, + evaluation: "Sum(56)".to_string(), + }; + let (text, json) = + plain_problem_output("CliTestAggregateValueSource", "brute-force", &result); + assert!(text.contains("Evaluation: Sum(56)"), "{text}"); + assert!(!text.contains("Solution:"), "{text}"); + assert!(json.get("solution").is_none(), "{json}"); + } + + #[test] + fn test_solve_bundle_rejects_aggregate_only_path() { + let bundle = aggregate_bundle(); + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = solve_bundle(bundle, "brute-force", &out).unwrap_err(); + assert!( + err.to_string().contains("witness"), + "unexpected error: {err}" + ); + } +} diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 53774c0f3..7a66c7fc7 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; use problemreductions::registry::{DynProblem, LoadedDynProblem}; -use problemreductions::rules::{MinimizeSteps, ReductionGraph}; +use problemreductions::rules::{MinimizeSteps, ReductionGraph, ReductionMode}; use problemreductions::solvers::ILPSolver; use problemreductions::types::ProblemSize; use serde_json::Value; @@ -37,12 +37,19 @@ impl std::ops::Deref for LoadedProblem { } impl LoadedProblem { - pub fn solve_brute_force(&self) -> Result { - let (config, evaluation) = self - .inner - .solve_brute_force() - .ok_or_else(|| anyhow::anyhow!("No solution found"))?; - Ok(SolveResult { config, evaluation }) + pub fn solve_brute_force_value(&self) -> String { + self.inner.solve_brute_force_value() + } + + pub fn solve_brute_force_witness(&self) -> Option { + let (config, evaluation) = self.inner.solve_brute_force_witness()?; + Some(WitnessSolveResult { config, evaluation }) + } + + pub fn solve_brute_force(&self) -> SolveResult { + let evaluation = self.solve_brute_force_value(); + let config = self.solve_brute_force_witness().map(|result| result.config); + SolveResult { config, evaluation } } pub fn supports_ilp_solver(&self) -> bool { @@ -54,28 +61,39 @@ impl LoadedProblem { let input_size = ProblemSize::new(vec![]); ilp_variants.iter().any(|dv| { graph - .find_cheapest_path(name, &variant, "ILP", dv, &input_size, &MinimizeSteps) + .find_cheapest_path_mode( + name, + &variant, + "ILP", + dv, + ReductionMode::Witness, + &input_size, + &MinimizeSteps, + ) .is_some() }) } } + #[cfg_attr(not(feature = "mcp"), allow(dead_code))] + pub fn available_solvers(&self) -> Vec<&'static str> { + let mut solvers = vec!["brute-force"]; + if self.supports_ilp_solver() { + solvers.push("ilp"); + } + solvers + } + /// Solve using the ILP solver. If the problem is not ILP, auto-reduce to ILP first. - pub fn solve_with_ilp(&self) -> Result { + pub fn solve_with_ilp(&self) -> Result { let name = self.problem_name(); let variant = self.variant_map(); let solver = ILPSolver::new(); let config = solver - .solve_via_reduction(name, &variant, self.as_any()) - .ok_or_else(|| { - anyhow::anyhow!( - "No reduction path from {} to ILP or ILP solver found no solution. \ - Try `--solver brute-force`.", - name - ) - })?; + .try_solve_via_reduction(name, &variant, self.as_any()) + .map_err(|err| anyhow::anyhow!(err))?; let evaluation = self.evaluate_dyn(&config); - Ok(SolveResult { config, evaluation }) + Ok(WitnessSolveResult { config, evaluation }) } } @@ -140,7 +158,17 @@ pub struct PathStep { } /// Result of solving a problem. +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SolveResult { + /// The solution configuration when the problem supports witness extraction. + pub config: Option>, + /// Evaluation of the solution. + pub evaluation: String, +} + +/// Result of solving a witness-capable problem. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WitnessSolveResult { /// The solution configuration. pub config: Vec, /// Evaluation of the solution. @@ -150,6 +178,7 @@ pub struct SolveResult { #[cfg(test)] mod tests { use super::*; + use crate::test_support::{AggregateValueSource, AGGREGATE_SOURCE_NAME}; use problemreductions::models::graph::MaximumIndependentSet; use problemreductions::models::misc::BinPacking; use problemreductions::topology::SimpleGraph; @@ -226,4 +255,34 @@ mod tests { "unexpected error: {err}" ); } + + #[test] + fn test_solve_brute_force_value_only_problem_has_no_witness() { + let loaded = load_problem( + AGGREGATE_SOURCE_NAME, + &BTreeMap::new(), + serde_json::to_value(AggregateValueSource::sample()).unwrap(), + ) + .unwrap(); + + let result = loaded.solve_brute_force(); + assert_eq!(result.config, None); + assert_eq!(result.evaluation, "Sum(56)"); + } + + #[test] + fn test_solve_with_ilp_rejects_aggregate_only_problem() { + let loaded = load_problem( + AGGREGATE_SOURCE_NAME, + &BTreeMap::new(), + serde_json::to_value(AggregateValueSource::sample()).unwrap(), + ) + .unwrap(); + + let err = loaded.solve_with_ilp().unwrap_err(); + assert!( + err.to_string().contains("witness-capable"), + "unexpected error: {err}" + ); + } } diff --git a/problemreductions-cli/src/main.rs b/problemreductions-cli/src/main.rs index b1f75d20f..ce4362132 100644 --- a/problemreductions-cli/src/main.rs +++ b/problemreductions-cli/src/main.rs @@ -5,6 +5,8 @@ mod dispatch; mod mcp; mod output; mod problem_name; +#[cfg(test)] +mod test_support; mod util; use clap::{CommandFactory, Parser}; diff --git a/problemreductions-cli/src/mcp/tests.rs b/problemreductions-cli/src/mcp/tests.rs index 715588dcd..117dd82c5 100644 --- a/problemreductions-cli/src/mcp/tests.rs +++ b/problemreductions-cli/src/mcp/tests.rs @@ -1,6 +1,7 @@ #[cfg(test)] mod tests { use crate::mcp::tools::McpServer; + use crate::test_support::{aggregate_bundle, aggregate_problem_json}; #[test] fn test_list_problems_returns_json() { @@ -430,4 +431,42 @@ mod tests { let json: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); assert_eq!(json["solver"], "brute-force"); } + + #[test] + fn test_reduce_rejects_aggregate_only_path() { + let server = McpServer::new(); + let result = server.reduce_inner(&aggregate_problem_json(), "CliTestAggregateValueTarget"); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("witness"), "unexpected error: {err}"); + } + + #[test] + fn test_solve_aggregate_only_problem_omits_solution() { + let server = McpServer::new(); + let result = server.solve_inner(&aggregate_problem_json(), Some("brute-force"), None); + assert!(result.is_ok()); + let json: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); + assert_eq!(json["evaluation"], "Sum(56)"); + assert!(json.get("solution").is_none(), "{json}"); + } + + #[test] + fn test_solve_ilp_rejects_aggregate_only_problem() { + let server = McpServer::new(); + let result = server.solve_inner(&aggregate_problem_json(), Some("ilp"), None); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("witness-capable"), "unexpected error: {err}"); + } + + #[test] + fn test_solve_bundle_rejects_aggregate_only_path() { + let server = McpServer::new(); + let bundle_json = serde_json::to_string(&aggregate_bundle()).unwrap(); + let result = server.solve_inner(&bundle_json, Some("brute-force"), None); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("witness"), "unexpected error: {err}"); + } } diff --git a/problemreductions-cli/src/mcp/tools.rs b/problemreductions-cli/src/mcp/tools.rs index 1172c36cb..b53f3d6dd 100644 --- a/problemreductions-cli/src/mcp/tools.rs +++ b/problemreductions-cli/src/mcp/tools.rs @@ -7,7 +7,9 @@ use problemreductions::models::graph::{ }; use problemreductions::models::misc::Factoring; use problemreductions::registry::collect_schemas; -use problemreductions::rules::{CustomCost, MinimizeSteps, ReductionGraph, TraversalDirection}; +use problemreductions::rules::{ + CustomCost, MinimizeSteps, ReductionGraph, ReductionMode, TraversalFlow, +}; use problemreductions::topology::{ Graph, KingsSubgraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph, }; @@ -39,7 +41,7 @@ pub struct NeighborsParams { pub problem: String, #[schemars(description = "Number of hops to explore (default: 1)")] pub hops: Option, - #[schemars(description = "Direction: out (default), in, or both")] + #[schemars(description = "Traversal direction: out (default), in, or both")] pub direction: Option, } @@ -811,23 +813,32 @@ impl McpServer { // Auto-discover cheapest path let input_size = ProblemSize::new(vec![]); - let best_path = graph.find_cheapest_path( + let best_path = graph.find_cheapest_path_mode( source_name, &source_variant, &dst_ref.name, &dst_ref.variant, + ReductionMode::Witness, &input_size, &MinimizeSteps, ); let reduction_path = best_path.ok_or_else(|| { - anyhow::anyhow!("No reduction path from {} to {}", source_name, dst_ref.name) + anyhow::anyhow!( + "No witness-capable reduction path from {} to {}", + source_name, + dst_ref.name + ) })?; // Execute reduction chain let chain = graph .reduce_along_path(&reduction_path, source.as_any()) - .ok_or_else(|| anyhow::anyhow!("Failed to execute reduction chain"))?; + .ok_or_else(|| { + anyhow::anyhow!( + "Reduction bundles require witness-capable paths; this path cannot produce a recoverable witness." + ) + })?; // Serialize target let target_step = reduction_path.steps.last().unwrap(); @@ -1094,11 +1105,11 @@ impl rmcp::ServerHandler for McpServer { // helpers // --------------------------------------------------------------------------- -fn parse_direction(s: &str) -> anyhow::Result { +fn parse_direction(s: &str) -> anyhow::Result { match s { - "out" => Ok(TraversalDirection::Outgoing), - "in" => Ok(TraversalDirection::Incoming), - "both" => Ok(TraversalDirection::Both), + "out" => Ok(TraversalFlow::Outgoing), + "in" => Ok(TraversalFlow::Incoming), + "both" => Ok(TraversalFlow::Both), _ => anyhow::bail!("Unknown direction: {}. Use 'out', 'in', or 'both'.", s), } } @@ -1147,6 +1158,22 @@ fn ser(problem: T) -> anyhow::Result { util::ser(problem) } +fn solve_result_json( + problem: &str, + solver: &str, + result: &crate::dispatch::SolveResult, +) -> serde_json::Value { + let mut json = serde_json::json!({ + "problem": problem, + "solver": solver, + "evaluation": result.evaluation, + }); + if let Some(config) = &result.config { + json["solution"] = serde_json::json!(config); + } + json +} + fn variant_map(pairs: &[(&str, &str)]) -> BTreeMap { util::variant_map(pairs) } @@ -1445,23 +1472,17 @@ fn solve_problem_inner( match solver_name { "brute-force" => { - let result = problem.solve_brute_force()?; - let json = serde_json::json!({ - "problem": name, - "solver": "brute-force", - "solution": result.config, - "evaluation": result.evaluation, - }); + let result = problem.solve_brute_force(); + let json = solve_result_json(name, "brute-force", &result); Ok(serde_json::to_string_pretty(&json)?) } "ilp" => { let result = problem.solve_with_ilp()?; - let mut json = serde_json::json!({ - "problem": name, - "solver": "ilp", - "solution": result.config, - "evaluation": result.evaluation, - }); + let result = crate::dispatch::SolveResult { + config: Some(result.config), + evaluation: result.evaluation, + }; + let mut json = solve_result_json(name, "ilp", &result); if name != "ILP" { json["reduced_to"] = serde_json::json!("ILP"); } @@ -1481,7 +1502,12 @@ fn solve_bundle_inner(bundle: ReductionBundle, solver_name: &str) -> anyhow::Res let target_name = target.problem_name(); let target_result = match solver_name { - "brute-force" => target.solve_brute_force()?, + "brute-force" => target.solve_brute_force_witness().ok_or_else(|| { + anyhow::anyhow!( + "Bundle solving requires a witness-capable target problem and witness-capable reduction path; {} only supports aggregate-value solving.", + target_name + ) + })?, "ilp" => target.solve_with_ilp()?, _ => unreachable!(), }; @@ -1508,9 +1534,9 @@ fn solve_bundle_inner(bundle: ReductionBundle, solver_name: &str) -> anyhow::Res let chain = graph .reduce_along_path(&reduction_path, source.as_any()) - .ok_or_else(|| { - anyhow::anyhow!("Failed to re-execute reduction chain for solution extraction") - })?; + .ok_or_else(|| anyhow::anyhow!( + "Bundle solving requires a witness-capable reduction path; this bundle cannot recover a source solution." + ))?; let source_config = chain.extract_solution(&target_result.config); let source_eval = source.evaluate_dyn(&source_config); diff --git a/problemreductions-cli/src/test_support.rs b/problemreductions-cli/src/test_support.rs new file mode 100644 index 000000000..81d3d33c8 --- /dev/null +++ b/problemreductions-cli/src/test_support.rs @@ -0,0 +1,241 @@ +use crate::dispatch::{PathStep, ProblemJsonOutput, ReductionBundle}; +use problemreductions::models::algebraic::{ObjectiveSense, ILP}; +use problemreductions::registry::VariantEntry; +use problemreductions::rules::registry::{EdgeCapabilities, ReductionEntry, ReductionOverhead}; +use problemreductions::rules::{AggregateReductionResult, ReductionAutoCast}; +use problemreductions::solvers::{BruteForce, Solver}; +use problemreductions::traits::Problem; +use problemreductions::types::{Extremum, ProblemSize, Sum}; +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::collections::BTreeMap; + +pub(crate) const AGGREGATE_SOURCE_NAME: &str = "CliTestAggregateValueSource"; +pub(crate) const AGGREGATE_TARGET_NAME: &str = "CliTestAggregateValueTarget"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct AggregateValueSource { + values: Vec, +} + +impl AggregateValueSource { + pub(crate) fn sample() -> Self { + Self { + values: vec![2, 5, 7], + } + } +} + +impl Problem for AggregateValueSource { + const NAME: &'static str = AGGREGATE_SOURCE_NAME; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![2; self.values.len()] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + let total = self + .values + .iter() + .zip(config.iter().copied()) + .filter_map(|(value, bit)| (bit == 1).then_some(*value)) + .sum(); + Sum(total) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct AggregateValueTarget { + base: u64, +} + +impl AggregateValueTarget { + pub(crate) fn sample() -> Self { + Self { base: 11 } + } +} + +impl Problem for AggregateValueTarget { + const NAME: &'static str = AGGREGATE_TARGET_NAME; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![2] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(self.base + config.iter().sum::() as u64) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +#[derive(Debug, Clone)] +struct AggregateValueToIlpReduction { + target: ILP, +} + +impl AggregateReductionResult for AggregateValueToIlpReduction { + type Source = AggregateValueSource; + type Target = ILP; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, _target_value: Extremum) -> Sum { + Sum(0) + } +} + +fn solve_value

(any: &dyn Any) -> String +where + P: Problem + Serialize + 'static, + P::Value: problemreductions::types::Aggregate + std::fmt::Display, +{ + let problem = any + .downcast_ref::

() + .expect("test solve_value downcast failed"); + let solver = BruteForce::new(); + problemreductions::registry::format_metric(&solver.solve(problem)) +} + +fn solve_witness

(any: &dyn Any) -> Option<(Vec, String)> +where + P: Problem + Serialize + 'static, + P::Value: problemreductions::types::Aggregate + std::fmt::Display, +{ + let problem = any.downcast_ref::

()?; + let solver = BruteForce::new(); + let config = solver.find_witness(problem)?; + let evaluation = problemreductions::registry::format_metric(&problem.evaluate(&config)); + Some((config, evaluation)) +} + +problemreductions::inventory::submit! { + VariantEntry { + name: AggregateValueSource::NAME, + variant_fn: AggregateValueSource::variant, + complexity: "2^num_values", + complexity_eval_fn: |_| 1.0, + is_default: true, + factory: |data| { + let problem: AggregateValueSource = serde_json::from_value(data)?; + Ok(Box::new(problem)) + }, + serialize_fn: |any| { + let problem = any.downcast_ref::()?; + Some(serde_json::to_value(problem).expect("serialize AggregateValueSource failed")) + }, + solve_value_fn: solve_value::, + solve_witness_fn: solve_witness::, + } +} + +problemreductions::inventory::submit! { + VariantEntry { + name: AggregateValueTarget::NAME, + variant_fn: AggregateValueTarget::variant, + complexity: "2", + complexity_eval_fn: |_| 1.0, + is_default: true, + factory: |data| { + let problem: AggregateValueTarget = serde_json::from_value(data)?; + Ok(Box::new(problem)) + }, + serialize_fn: |any| { + let problem = any.downcast_ref::()?; + Some(serde_json::to_value(problem).expect("serialize AggregateValueTarget failed")) + }, + solve_value_fn: solve_value::, + solve_witness_fn: solve_witness::, + } +} + +problemreductions::inventory::submit! { + ReductionEntry { + source_name: AggregateValueSource::NAME, + target_name: AggregateValueTarget::NAME, + source_variant_fn: AggregateValueSource::variant, + target_variant_fn: AggregateValueTarget::variant, + overhead_fn: || ReductionOverhead::default(), + module_path: module_path!(), + reduce_fn: None, + reduce_aggregate_fn: Some(|any: &dyn Any| { + let source = any + .downcast_ref::() + .expect("aggregate reduction downcast failed"); + Box::new(ReductionAutoCast::::new( + AggregateValueTarget { + base: source.values.iter().sum(), + }, + )) + }), + capabilities: EdgeCapabilities::aggregate_only(), + overhead_eval_fn: |_| ProblemSize::new(vec![]), + } +} + +problemreductions::inventory::submit! { + ReductionEntry { + source_name: AggregateValueSource::NAME, + target_name: ILP::::NAME, + source_variant_fn: AggregateValueSource::variant, + target_variant_fn: ILP::::variant, + overhead_fn: || ReductionOverhead::default(), + module_path: module_path!(), + reduce_fn: None, + reduce_aggregate_fn: Some(|any: &dyn Any| { + let _source = any + .downcast_ref::() + .expect("aggregate ILP reduction downcast failed"); + Box::new(AggregateValueToIlpReduction { + target: ILP::new(0, vec![], vec![], ObjectiveSense::Minimize), + }) + }), + capabilities: EdgeCapabilities::aggregate_only(), + overhead_eval_fn: |_| ProblemSize::new(vec![]), + } +} + +#[cfg_attr(not(feature = "mcp"), allow(dead_code))] +pub(crate) fn aggregate_problem_json() -> String { + serde_json::to_string(&ProblemJsonOutput { + problem_type: AggregateValueSource::NAME.to_string(), + variant: BTreeMap::new(), + data: serde_json::to_value(AggregateValueSource::sample()).unwrap(), + }) + .unwrap() +} + +pub(crate) fn aggregate_bundle() -> ReductionBundle { + ReductionBundle { + source: ProblemJsonOutput { + problem_type: AggregateValueSource::NAME.to_string(), + variant: BTreeMap::new(), + data: serde_json::to_value(AggregateValueSource::sample()).unwrap(), + }, + target: ProblemJsonOutput { + problem_type: AggregateValueTarget::NAME.to_string(), + variant: BTreeMap::new(), + data: serde_json::to_value(AggregateValueTarget::sample()).unwrap(), + }, + path: vec![ + PathStep { + name: AggregateValueSource::NAME.to_string(), + variant: BTreeMap::new(), + }, + PathStep { + name: AggregateValueTarget::NAME.to_string(), + variant: BTreeMap::new(), + }, + ], + } +} diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 7a95af09b..35befb32c 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -387,7 +387,7 @@ fn test_evaluate() { String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).unwrap(); - assert!(stdout.contains("Valid")); + assert!(stdout.contains("Max(2)"), "stdout: {stdout}"); std::fs::remove_file(&tmp).ok(); } @@ -2235,7 +2235,7 @@ fn test_create_then_evaluate() { String::from_utf8_lossy(&eval_output.stderr) ); let stdout = String::from_utf8(eval_output.stdout).unwrap(); - assert!(stdout.contains("Valid")); + assert!(stdout.contains("Max(2)"), "stdout: {stdout}"); std::fs::remove_file(&problem_file).ok(); } @@ -5756,8 +5756,8 @@ fn test_create_pipe_to_evaluate() { ); let stdout = String::from_utf8(eval_result.stdout).unwrap(); assert!( - stdout.contains("Valid"), - "stdout should contain Valid, got: {stdout}" + stdout.contains("Max("), + "stdout should contain Max(...), got: {stdout}" ); } @@ -8226,7 +8226,7 @@ fn test_create_weighted_mis_round_trips_into_solve() { ); let stdout = String::from_utf8(solve_output.stdout).unwrap(); let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); - assert_eq!(json["evaluation"], "Valid(5)"); + assert_eq!(json["evaluation"], "Max(5)"); } #[test] diff --git a/problemreductions-macros/src/lib.rs b/problemreductions-macros/src/lib.rs index 0d790967f..0cbdf482c 100644 --- a/problemreductions-macros/src/lib.rs +++ b/problemreductions-macros/src/lib.rs @@ -275,6 +275,11 @@ fn generate_reduction_entry( .ok_or_else(|| syn::Error::new_spanned(source_type, "Cannot extract source type name"))?; let target_name = extract_type_name(&target_type) .ok_or_else(|| syn::Error::new_spanned(&target_type, "Cannot extract target type name"))?; + let capabilities = if source_name == target_name { + quote! { crate::rules::EdgeCapabilities::both() } + } else { + quote! { crate::rules::EdgeCapabilities::witness_only() } + }; // Collect generic parameter info from the impl block let type_generics = collect_type_generic_names(&impl_block.generics); @@ -319,7 +324,7 @@ fn generate_reduction_entry( target_variant_fn: || { #target_variant_body }, overhead_fn: || { #overhead }, module_path: module_path!(), - reduce_fn: |src: &dyn std::any::Any| -> Box { + reduce_fn: Some(|src: &dyn std::any::Any| -> Box { let src = src.downcast_ref::<#source_type>().unwrap_or_else(|| { panic!( "DynReductionResult: source type mismatch: expected `{}`, got `{}`", @@ -328,7 +333,9 @@ fn generate_reduction_entry( ) }); Box::new(<#source_type as crate::rules::ReduceTo<#target_type>>::reduce_to(src)) - }, + }), + reduce_aggregate_fn: None, + capabilities: #capabilities, overhead_eval_fn: #overhead_eval_fn, } } @@ -370,24 +377,14 @@ fn extract_target_from_trait(path: &Path) -> syn::Result { // --- declare_variants! proc macro --- -/// Solver kind for dispatch generation. -#[derive(Debug, Clone, Copy)] -enum SolverKind { - /// Optimization problem — uses `find_best`. - Opt, - /// Satisfaction problem — uses `find_satisfying`. - Sat, -} - /// Input for the `declare_variants!` proc macro. struct DeclareVariantsInput { entries: Vec, } -/// A single entry: `[default] opt|sat Type => "complexity_string"`. +/// A single entry: `[default] Type => "complexity_string"`. struct DeclareVariantEntry { is_default: bool, - solver_kind: SolverKind, ty: Type, complexity: syn::LitStr, } @@ -402,39 +399,11 @@ impl syn::parse::Parse for DeclareVariantsInput { input.parse::()?; } - // Require `opt` or `sat` keyword - let solver_kind = if input.peek(syn::Ident) { - let fork = input.fork(); - if let Ok(ident) = fork.parse::() { - match ident.to_string().as_str() { - "opt" => { - input.parse::()?; // consume - SolverKind::Opt - } - "sat" => { - input.parse::()?; // consume - SolverKind::Sat - } - _ => { - return Err(syn::Error::new( - ident.span(), - "expected `opt` or `sat` before type name", - )); - } - } - } else { - return Err(input.error("expected `opt` or `sat` before type name")); - } - } else { - return Err(input.error("expected `opt` or `sat` before type name")); - }; - let ty: Type = input.parse()?; input.parse::]>()?; let complexity: syn::LitStr = input.parse()?; entries.push(DeclareVariantEntry { is_default, - solver_kind, ty, complexity, }); @@ -557,14 +526,14 @@ fn generate_declare_variants(input: &DeclareVariantsInput) -> syn::Result quote! { - let config = ::find_best(&solver, p)?; - }, - SolverKind::Sat => quote! { - let config = ::find_satisfying(&solver, p)?; - }, + // Generate dispatch fields based on aggregate value solving plus optional witnesses. + let solve_value_body = quote! { + let total = ::solve(&solver, p); + crate::registry::format_metric(&total) + }; + + let solve_witness_body = quote! { + let config = crate::solvers::BruteForce::find_witness(&solver, p)?; }; let dispatch_fields = quote! { @@ -576,11 +545,18 @@ fn generate_declare_variants(input: &DeclareVariantsInput) -> syn::Result()?; Some(serde_json::to_value(p).expect("serialize failed")) }, - solve_fn: |any: &dyn std::any::Any| -> Option<(Vec, String)> { + solve_value_fn: |any: &dyn std::any::Any| -> String { + let p = any + .downcast_ref::<#ty>() + .expect("type-erased solve_value downcast failed"); + let solver = crate::solvers::BruteForce::new(); + #solve_value_body + }, + solve_witness_fn: |any: &dyn std::any::Any| -> Option<(Vec, String)> { let p = any.downcast_ref::<#ty>()?; let solver = crate::solvers::BruteForce::new(); - #solve_body - let evaluation = format!("{:?}", crate::traits::Problem::evaluate(p, &config)); + #solve_witness_body + let evaluation = crate::registry::format_metric(&crate::traits::Problem::evaluate(p, &config)); Some((config, evaluation)) }, }; @@ -632,7 +608,7 @@ mod tests { #[test] fn declare_variants_accepts_single_default() { let input: DeclareVariantsInput = syn::parse_quote! { - default opt Foo => "1", + default Foo => "1", }; assert!(generate_declare_variants(&input).is_ok()); } @@ -640,7 +616,7 @@ mod tests { #[test] fn declare_variants_requires_one_default_per_problem() { let input: DeclareVariantsInput = syn::parse_quote! { - opt Foo => "1", + Foo => "1", }; let err = generate_declare_variants(&input).unwrap_err(); assert!( @@ -653,8 +629,8 @@ mod tests { #[test] fn declare_variants_rejects_multiple_defaults_for_one_problem() { let input: DeclareVariantsInput = syn::parse_quote! { - default opt Foo => "1", - default opt Foo => "2", + default Foo => "1", + default Foo => "2", }; let err = generate_declare_variants(&input).unwrap_err(); assert!( @@ -667,7 +643,7 @@ mod tests { #[test] fn declare_variants_rejects_missing_default_marker() { let input: DeclareVariantsInput = syn::parse_quote! { - opt Foo => "1", + Foo => "1", }; let err = generate_declare_variants(&input).unwrap_err(); assert!( @@ -680,8 +656,8 @@ mod tests { #[test] fn declare_variants_marks_only_explicit_default() { let input: DeclareVariantsInput = syn::parse_quote! { - opt Foo => "1", - default opt Foo => "2", + Foo => "1", + default Foo => "2", }; let result = generate_declare_variants(&input); assert!(result.is_ok()); @@ -693,27 +669,27 @@ mod tests { } #[test] - fn declare_variants_accepts_solver_kind_markers() { + fn declare_variants_accepts_entries_without_solver_kind_markers() { let input: DeclareVariantsInput = syn::parse_quote! { - default opt Foo => "1", - default sat Bar => "2", + default Foo => "1", + default Bar => "2", }; assert!(generate_declare_variants(&input).is_ok()); } #[test] - fn declare_variants_rejects_missing_solver_kind() { - let result = syn::parse_str::("Foo => \"1\""); + fn declare_variants_rejects_legacy_solver_kind_markers() { + let result = syn::parse_str::("default opt Foo => \"1\""); assert!( result.is_err(), - "expected parse error for missing solver kind" + "expected parse error for legacy solver kind marker" ); } #[test] - fn declare_variants_generates_find_best_for_opt_entries() { + fn declare_variants_generates_aggregate_value_and_witness_dispatch() { let input: DeclareVariantsInput = syn::parse_quote! { - default opt Foo => "1", + default Foo => "1", }; let tokens = generate_declare_variants(&input).unwrap().to_string(); assert!(tokens.contains("factory :"), "expected factory field"); @@ -721,7 +697,14 @@ mod tests { tokens.contains("serialize_fn :"), "expected serialize_fn field" ); - assert!(tokens.contains("solve_fn :"), "expected solve_fn field"); + assert!( + tokens.contains("solve_value_fn :"), + "expected solve_value_fn field" + ); + assert!( + tokens.contains("solve_witness_fn :"), + "expected solve_witness_fn field" + ); assert!( !tokens.contains("factory : None"), "factory should not be None" @@ -731,39 +714,28 @@ mod tests { "serialize_fn should not be None" ); assert!( - !tokens.contains("solve_fn : None"), - "solve_fn should not be None" + !tokens.contains("solve_value_fn : None"), + "solve_value_fn should not be None" ); - assert!(tokens.contains("find_best"), "expected find_best in tokens"); - } - - #[test] - fn declare_variants_generates_find_satisfying_for_sat_entries() { - let input: DeclareVariantsInput = syn::parse_quote! { - default sat Foo => "1", - }; - let tokens = generate_declare_variants(&input).unwrap().to_string(); - assert!(tokens.contains("factory :"), "expected factory field"); assert!( - tokens.contains("serialize_fn :"), - "expected serialize_fn field" + !tokens.contains("solve_witness_fn : None"), + "solve_witness_fn should not be None" ); - assert!(tokens.contains("solve_fn :"), "expected solve_fn field"); assert!( - !tokens.contains("factory : None"), - "factory should not be None" + tokens.contains("let total ="), + "expected aggregate value solve" ); assert!( - !tokens.contains("serialize_fn : None"), - "serialize_fn should not be None" + tokens.contains("find_witness"), + "expected find_witness in tokens" ); assert!( - !tokens.contains("solve_fn : None"), - "solve_fn should not be None" + !tokens.contains("find_best"), + "did not expect legacy find_best in tokens" ); assert!( - tokens.contains("find_satisfying"), - "expected find_satisfying in tokens" + !tokens.contains("SolutionSize :: Invalid"), + "did not expect legacy invalid fallback in tokens" ); } @@ -791,14 +763,16 @@ mod tests { #[test] fn declare_variants_codegen_uses_required_dispatch_fields() { let input: DeclareVariantsInput = syn::parse_quote! { - default opt Foo => "1", + default Foo => "1", }; let tokens = generate_declare_variants(&input).unwrap().to_string(); assert!(tokens.contains("factory :")); assert!(tokens.contains("serialize_fn :")); - assert!(tokens.contains("solve_fn :")); + assert!(tokens.contains("solve_value_fn :")); + assert!(tokens.contains("solve_witness_fn :")); assert!(!tokens.contains("factory : None")); assert!(!tokens.contains("serialize_fn : None")); - assert!(!tokens.contains("solve_fn : None")); + assert!(!tokens.contains("solve_value_fn : None")); + assert!(!tokens.contains("solve_witness_fn : None")); } } diff --git a/src/lib.rs b/src/lib.rs index 56b6902ea..2eca06109 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,8 +11,8 @@ //! | [`rules`] | Reduction rules, [`ReductionGraph`](rules::ReductionGraph) for path search | //! | [`solvers`] | [`BruteForce`] and [`ILPSolver`](solvers::ILPSolver) | //! | [`topology`] | Graph types — [`SimpleGraph`](topology::SimpleGraph), [`UnitDiskGraph`](topology::UnitDiskGraph), etc. | -//! | [`traits`] | Core traits — [`Problem`], [`OptimizationProblem`], [`SatisfactionProblem`] | -//! | [`types`] | [`SolutionSize`], [`Direction`], [`ProblemSize`], [`WeightElement`] | +//! | [`traits`] | Core traits — [`Problem`] | +//! | [`types`] | [`Max`], [`Min`], [`Extremum`], [`ExtremumSense`], [`ProblemSize`], [`WeightElement`] | //! | [`variant`] | Variant parameter system for problem type parameterization | //! //! Use [`prelude`] for convenient imports. @@ -89,11 +89,13 @@ pub mod prelude { // Core traits pub use crate::rules::{ReduceTo, ReductionResult}; pub use crate::solvers::{BruteForce, Solver}; - pub use crate::traits::{OptimizationProblem, Problem, SatisfactionProblem}; + pub use crate::traits::Problem; // Types pub use crate::error::{ProblemError, Result}; - pub use crate::types::{Direction, One, ProblemSize, SolutionSize, Unweighted}; + pub use crate::types::{ + And, Extremum, ExtremumSense, Max, Min, One, Or, ProblemSize, Sum, Unweighted, + }; } // Re-export commonly used items at crate root @@ -103,9 +105,10 @@ pub use error::{ProblemError, Result}; pub use expr::{asymptotic_normal_form, AsymptoticAnalysisError, CanonicalizationError, Expr}; pub use registry::{ComplexityClass, ProblemInfo}; pub use solvers::{BruteForce, Solver}; -pub use traits::{OptimizationProblem, Problem, SatisfactionProblem}; +pub use traits::Problem; pub use types::{ - Direction, NumericSize, One, ProblemSize, SolutionSize, Unweighted, WeightElement, + And, Extremum, ExtremumSense, Max, Min, NumericSize, One, Or, ProblemSize, Sum, Unweighted, + WeightElement, }; // Re-export proc macros for reduction registration and variant declaration diff --git a/src/models/algebraic/bmf.rs b/src/models/algebraic/bmf.rs index 74d011a06..8e3bc1f81 100644 --- a/src/models/algebraic/bmf.rs +++ b/src/models/algebraic/bmf.rs @@ -5,8 +5,8 @@ //! The boolean product `(B * C)[i,j] = OR_k (B[i,k] AND C[k,j])`. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -46,7 +46,7 @@ inventory::submit! { /// let problem = BMF::new(a, 1); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Check the error /// for sol in &solutions { @@ -205,17 +205,17 @@ pub(crate) fn matrix_hamming_distance(a: &[Vec], b: &[Vec]) -> usize impl Problem for BMF { const NAME: &'static str = "BMF"; - type Metric = SolutionSize; + type Value = Min; fn dims(&self) -> Vec { // B: m*k + C: k*n binary variables vec![2; self.m * self.k + self.k * self.n] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { // Minimize Hamming distance between A and B*C. // All configurations are valid -- the distance is the objective. - SolutionSize::Valid(self.hamming_distance(config) as i32) + Min(Some(self.hamming_distance(config) as i32)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -223,16 +223,8 @@ impl Problem for BMF { } } -impl OptimizationProblem for BMF { - type Value = i32; - - fn direction(&self) -> Direction { - Direction::Minimize - } -} - crate::declare_variants! { - default opt BMF => "2^(rows * rank + rank * cols)", + default BMF => "2^(rows * rank + rank * cols)", } #[cfg(feature = "example-db")] @@ -248,7 +240,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn dims(&self) -> Vec { self.bounds @@ -253,7 +253,7 @@ where .collect() } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let values = self.config_to_values(config); let m = self.ambient_dimension(); let mut diff = vec![0.0f64; m]; @@ -266,7 +266,7 @@ where *d -= t; } let norm = diff.iter().map(|d| d * d).sum::().sqrt(); - SolutionSize::Valid(norm) + Min(Some(norm)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -274,26 +274,9 @@ where } } -impl OptimizationProblem for ClosestVectorProblem -where - T: Clone - + Into - + crate::variant::VariantParam - + Serialize - + for<'de> Deserialize<'de> - + std::fmt::Debug - + 'static, -{ - type Value = f64; - - fn direction(&self) -> Direction { - Direction::Minimize - } -} - crate::declare_variants! { - default opt ClosestVectorProblem => "2^num_basis_vectors", - opt ClosestVectorProblem => "2^num_basis_vectors", + default ClosestVectorProblem => "2^num_basis_vectors", + ClosestVectorProblem => "2^num_basis_vectors", } #[cfg(feature = "example-db")] @@ -306,7 +289,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec { vec![self.num_cols; self.num_cols] } - fn evaluate(&self, config: &[usize]) -> bool { - match self.count_consecutive_blocks(config) { - Some(total) => (total as i64) <= self.bound, - None => false, - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + match self.count_consecutive_blocks(config) { + Some(total) => (total as i64) <= self.bound, + None => false, + } + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -178,10 +180,8 @@ impl Problem for ConsecutiveBlockMinimization { } } -impl SatisfactionProblem for ConsecutiveBlockMinimization {} - crate::declare_variants! { - default sat ConsecutiveBlockMinimization => "factorial(num_cols) * num_rows * num_cols", + default ConsecutiveBlockMinimization => "factorial(num_cols) * num_rows * num_cols", } #[derive(Debug, Clone, Deserialize)] diff --git a/src/models/algebraic/consecutive_ones_matrix_augmentation.rs b/src/models/algebraic/consecutive_ones_matrix_augmentation.rs index aa5d187a0..079d7bcd6 100644 --- a/src/models/algebraic/consecutive_ones_matrix_augmentation.rs +++ b/src/models/algebraic/consecutive_ones_matrix_augmentation.rs @@ -5,7 +5,7 @@ //! augmentations such that every row has consecutive 1s. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -114,15 +114,17 @@ impl ConsecutiveOnesMatrixAugmentation { impl Problem for ConsecutiveOnesMatrixAugmentation { const NAME: &'static str = "ConsecutiveOnesMatrixAugmentation"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.num_cols(); self.num_cols()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.total_augmentation_cost(config) - .is_some_and(|cost| cost <= self.bound as usize) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + self.total_augmentation_cost(config) + .is_some_and(|cost| cost <= self.bound as usize) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -134,10 +136,8 @@ impl Problem for ConsecutiveOnesMatrixAugmentation { } } -impl SatisfactionProblem for ConsecutiveOnesMatrixAugmentation {} - crate::declare_variants! { - default sat ConsecutiveOnesMatrixAugmentation => "factorial(num_cols) * num_rows * num_cols", + default ConsecutiveOnesMatrixAugmentation => "factorial(num_cols) * num_rows * num_cols", } #[cfg(feature = "example-db")] diff --git a/src/models/algebraic/consecutive_ones_submatrix.rs b/src/models/algebraic/consecutive_ones_submatrix.rs index 03b0feb91..3b7308ddc 100644 --- a/src/models/algebraic/consecutive_ones_submatrix.rs +++ b/src/models/algebraic/consecutive_ones_submatrix.rs @@ -7,7 +7,7 @@ //! transformation from Hamiltonian Path. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -54,7 +54,7 @@ inventory::submit! { /// ]; /// let problem = ConsecutiveOnesSubmatrix::new(matrix, 3); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -173,7 +173,7 @@ impl ConsecutiveOnesSubmatrix { impl Problem for ConsecutiveOnesSubmatrix { const NAME: &'static str = "ConsecutiveOnesSubmatrix"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -183,31 +183,31 @@ impl Problem for ConsecutiveOnesSubmatrix { vec![2; self.num_cols()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_cols() { - return false; - } - if config.iter().any(|&v| v >= 2) { - return false; - } - // Collect selected column indices - let selected: Vec = config - .iter() - .enumerate() - .filter(|(_, &v)| v == 1) - .map(|(i, _)| i) - .collect(); - if (selected.len() as i64) != self.bound { - return false; - } - self.any_permutation_has_c1p(&selected) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_cols() { + return crate::types::Or(false); + } + if config.iter().any(|&v| v >= 2) { + return crate::types::Or(false); + } + // Collect selected column indices + let selected: Vec = config + .iter() + .enumerate() + .filter(|(_, &v)| v == 1) + .map(|(i, _)| i) + .collect(); + if (selected.len() as i64) != self.bound { + return crate::types::Or(false); + } + self.any_permutation_has_c1p(&selected) + }) } } -impl SatisfactionProblem for ConsecutiveOnesSubmatrix {} - crate::declare_variants! { - default sat ConsecutiveOnesSubmatrix => "2^(num_cols) * (num_rows + num_cols)", + default ConsecutiveOnesSubmatrix => "2^(num_cols) * (num_rows + num_cols)", } #[cfg(feature = "example-db")] diff --git a/src/models/algebraic/ilp.rs b/src/models/algebraic/ilp.rs index 5dab49674..adcb80d15 100644 --- a/src/models/algebraic/ilp.rs +++ b/src/models/algebraic/ilp.rs @@ -8,8 +8,8 @@ //! - `ILP`: non-negative integer variables (0..2^31-1) use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Extremum; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; @@ -244,18 +244,25 @@ impl ILP { impl Problem for ILP { const NAME: &'static str = "ILP"; - type Metric = SolutionSize; + type Value = Extremum; fn dims(&self) -> Vec { vec![V::DIMS_PER_VAR; self.num_vars] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Extremum { let values = self.config_to_values(config); if !self.is_feasible(&values) { - return SolutionSize::Invalid; + return match self.sense { + ObjectiveSense::Maximize => Extremum::maximize(None), + ObjectiveSense::Minimize => Extremum::minimize(None), + }; + } + let objective = self.evaluate_objective(&values); + match self.sense { + ObjectiveSense::Maximize => Extremum::maximize(Some(objective)), + ObjectiveSense::Minimize => Extremum::minimize(Some(objective)), } - SolutionSize::Valid(self.evaluate_objective(&values)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -263,20 +270,9 @@ impl Problem for ILP { } } -impl OptimizationProblem for ILP { - type Value = f64; - - fn direction(&self) -> Direction { - match self.sense { - ObjectiveSense::Maximize => Direction::Maximize, - ObjectiveSense::Minimize => Direction::Minimize, - } - } -} - crate::declare_variants! { - default opt ILP => "2^num_vars", - opt ILP => "num_vars^num_vars", + default ILP => "2^num_vars", + ILP => "num_vars^num_vars", } #[cfg(feature = "example-db")] @@ -293,7 +289,10 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn dims(&self) -> Vec { vec![self.num_locations(); self.num_facilities()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let n = self.num_facilities(); let m = self.num_locations(); // Check config length matches number of facilities if config.len() != n { - return SolutionSize::Invalid; + return Min(None); } // Check that all assignments are valid locations for &loc in config { if loc >= m { - return SolutionSize::Invalid; + return Min(None); } } @@ -141,7 +141,7 @@ impl Problem for QuadraticAssignment { let mut used = vec![false; m]; for &loc in config { if used[loc] { - return SolutionSize::Invalid; + return Min(None); } used[loc] = true; } @@ -156,7 +156,7 @@ impl Problem for QuadraticAssignment { } } - SolutionSize::Valid(total) + Min(Some(total)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -164,16 +164,8 @@ impl Problem for QuadraticAssignment { } } -impl OptimizationProblem for QuadraticAssignment { - type Value = i64; - - fn direction(&self) -> Direction { - Direction::Minimize - } -} - crate::declare_variants! { - default opt QuadraticAssignment => "factorial(num_facilities)", + default QuadraticAssignment => "factorial(num_facilities)", } #[cfg(feature = "example-db")] @@ -195,7 +187,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec, { const NAME: &'static str = "QUBO"; - type Metric = SolutionSize; + type Value = Min; fn dims(&self) -> Vec { vec![2; self.num_vars] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { - SolutionSize::Valid(self.evaluate(config).to_sum()) + fn evaluate(&self, config: &[usize]) -> Min { + Min(Some(self.evaluate(config).to_sum())) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -173,26 +173,8 @@ where } } -impl OptimizationProblem for QUBO -where - W: WeightElement - + crate::variant::VariantParam - + PartialOrd - + num_traits::Num - + num_traits::Zero - + num_traits::Bounded - + std::ops::AddAssign - + std::ops::Mul, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Minimize - } -} - crate::declare_variants! { - default opt QUBO => "2^num_vars", + default QUBO => "2^num_vars", } #[cfg(feature = "example-db")] @@ -205,7 +187,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec { vec![self.bound_k; self.num_rows()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.storage_vector(config).is_some() + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.storage_vector(config).is_some()) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -134,10 +134,8 @@ impl Problem for SparseMatrixCompression { } } -impl SatisfactionProblem for SparseMatrixCompression {} - crate::declare_variants! { - default sat SparseMatrixCompression => "(bound_k ^ num_rows) * num_rows * num_cols", + default SparseMatrixCompression => "(bound_k ^ num_rows) * num_rows * num_cols", } #[cfg(feature = "example-db")] diff --git a/src/models/formula/circuit.rs b/src/models/formula/circuit.rs index f252c18b0..1a951265c 100644 --- a/src/models/formula/circuit.rs +++ b/src/models/formula/circuit.rs @@ -4,7 +4,7 @@ //! The goal is to find variable assignments that satisfy the circuit constraints. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -210,7 +210,7 @@ impl Circuit { /// /// let problem = CircuitSAT::new(circuit); /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_satisfying(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Multiple satisfying assignments exist /// assert!(!solutions.is_empty()); @@ -289,14 +289,14 @@ pub(crate) fn is_circuit_satisfying( impl Problem for CircuitSAT { const NAME: &'static str = "CircuitSAT"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.variables.len()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.count_satisfied(config) == self.circuit.num_assignments() + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.count_satisfied(config) == self.circuit.num_assignments()) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -304,10 +304,8 @@ impl Problem for CircuitSAT { } } -impl SatisfactionProblem for CircuitSAT {} - crate::declare_variants! { - default sat CircuitSAT => "2^num_variables", + default CircuitSAT => "2^num_variables", } #[cfg(feature = "example-db")] diff --git a/src/models/formula/ksat.rs b/src/models/formula/ksat.rs index 0b9681a7c..047c902e5 100644 --- a/src/models/formula/ksat.rs +++ b/src/models/formula/ksat.rs @@ -6,7 +6,7 @@ //! MaxKSatisfiability type (if available). use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::variant::{KValue, K2, K3, KN}; use serde::{Deserialize, Serialize}; @@ -54,7 +54,7 @@ inventory::submit! { /// ); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_satisfying(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// assert!(!solutions.is_empty()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -168,15 +168,17 @@ impl KSatisfiability { impl Problem for KSatisfiability { const NAME: &'static str = "KSatisfiability"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.num_vars] } - fn evaluate(&self, config: &[usize]) -> bool { - let assignment = Self::config_to_assignment(config); - self.is_satisfying(&assignment) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let assignment = Self::config_to_assignment(config); + self.is_satisfying(&assignment) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -184,12 +186,10 @@ impl Problem for KSatisfiability { } } -impl SatisfactionProblem for KSatisfiability {} - crate::declare_variants! { - default sat KSatisfiability => "2^num_variables", - sat KSatisfiability => "num_variables + num_clauses", - sat KSatisfiability => "1.307^num_variables", + default KSatisfiability => "2^num_variables", + KSatisfiability => "num_variables + num_clauses", + KSatisfiability => "1.307^num_variables", } #[cfg(feature = "example-db")] diff --git a/src/models/formula/nae_satisfiability.rs b/src/models/formula/nae_satisfiability.rs index 54e896a4a..cbc53b327 100644 --- a/src/models/formula/nae_satisfiability.rs +++ b/src/models/formula/nae_satisfiability.rs @@ -4,7 +4,7 @@ //! contains at least one true literal and at least one false literal. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use super::CNFClause; @@ -96,7 +96,7 @@ impl NAESatisfiability { /// Check if a solution (config) is valid. pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } fn config_to_assignment(config: &[usize]) -> Vec { @@ -135,15 +135,17 @@ impl NAESatisfiability { impl Problem for NAESatisfiability { const NAME: &'static str = "NAESatisfiability"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.num_vars] } - fn evaluate(&self, config: &[usize]) -> bool { - let assignment = Self::config_to_assignment(config); - self.is_nae_satisfying(&assignment) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let assignment = Self::config_to_assignment(config); + self.is_nae_satisfying(&assignment) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -151,10 +153,8 @@ impl Problem for NAESatisfiability { } } -impl SatisfactionProblem for NAESatisfiability {} - crate::declare_variants! { - default sat NAESatisfiability => "2^num_variables", + default NAESatisfiability => "2^num_variables", } #[derive(Debug, Clone, Deserialize)] diff --git a/src/models/formula/qbf.rs b/src/models/formula/qbf.rs index 830ddc167..d202e9f17 100644 --- a/src/models/formula/qbf.rs +++ b/src/models/formula/qbf.rs @@ -10,7 +10,7 @@ use crate::models::formula::CNFClause; use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -157,17 +157,19 @@ impl QuantifiedBooleanFormulas { impl Problem for QuantifiedBooleanFormulas { const NAME: &'static str = "QuantifiedBooleanFormulas"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![] } - fn evaluate(&self, config: &[usize]) -> bool { - if !config.is_empty() { - return false; - } - self.is_true() + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if !config.is_empty() { + return crate::types::Or(false); + } + self.is_true() + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -175,10 +177,8 @@ impl Problem for QuantifiedBooleanFormulas { } } -impl SatisfactionProblem for QuantifiedBooleanFormulas {} - crate::declare_variants! { - default sat QuantifiedBooleanFormulas => "2^num_vars", + default QuantifiedBooleanFormulas => "2^num_vars", } #[cfg(feature = "example-db")] diff --git a/src/models/formula/sat.rs b/src/models/formula/sat.rs index 17272080c..d84f35940 100644 --- a/src/models/formula/sat.rs +++ b/src/models/formula/sat.rs @@ -6,7 +6,7 @@ //! the separate MaxSatisfiability type (if available). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -106,7 +106,7 @@ impl CNFClause { /// ); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_satisfying(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Verify solutions satisfy all clauses /// for sol in solutions { @@ -169,7 +169,7 @@ impl Satisfiability { /// /// For SAT, a valid solution is one that satisfies all clauses. pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } /// Convert a usize config to boolean assignment. @@ -180,15 +180,17 @@ impl Satisfiability { impl Problem for Satisfiability { const NAME: &'static str = "Satisfiability"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.num_vars] } - fn evaluate(&self, config: &[usize]) -> bool { - let assignment = Self::config_to_assignment(config); - self.is_satisfying(&assignment) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let assignment = Self::config_to_assignment(config); + self.is_satisfying(&assignment) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -196,10 +198,8 @@ impl Problem for Satisfiability { } } -impl SatisfactionProblem for Satisfiability {} - crate::declare_variants! { - default sat Satisfiability => "2^num_variables", + default Satisfiability => "2^num_variables", } /// Check if an assignment satisfies a SAT formula. diff --git a/src/models/graph/acyclic_partition.rs b/src/models/graph/acyclic_partition.rs index c81725854..4bb5f4935 100644 --- a/src/models/graph/acyclic_partition.rs +++ b/src/models/graph/acyclic_partition.rs @@ -7,7 +7,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry, VariantDimension}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -156,7 +156,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "AcyclicPartition"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -166,23 +166,20 @@ where vec![self.graph.num_vertices(); self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - is_valid_acyclic_partition( - &self.graph, - &self.vertex_weights, - &self.arc_costs, - &self.weight_bound, - &self.cost_bound, - config, - ) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + is_valid_acyclic_partition( + &self.graph, + &self.vertex_weights, + &self.arc_costs, + &self.weight_bound, + &self.cost_bound, + config, + ) + }) } } -impl SatisfactionProblem for AcyclicPartition where - W: WeightElement + crate::variant::VariantParam -{ -} - fn is_valid_acyclic_partition( graph: &DirectedGraph, vertex_weights: &[W], @@ -240,7 +237,7 @@ fn is_valid_acyclic_partition( } crate::declare_variants! { - default sat AcyclicPartition => "num_vertices^num_vertices", + default AcyclicPartition => "num_vertices^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/balanced_complete_bipartite_subgraph.rs b/src/models/graph/balanced_complete_bipartite_subgraph.rs index dbb46d4e5..fe2ce502f 100644 --- a/src/models/graph/balanced_complete_bipartite_subgraph.rs +++ b/src/models/graph/balanced_complete_bipartite_subgraph.rs @@ -1,6 +1,6 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::BipartiteGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -96,31 +96,33 @@ impl BalancedCompleteBipartiteSubgraph { } pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } } impl Problem for BalancedCompleteBipartiteSubgraph { const NAME: &'static str = "BalancedCompleteBipartiteSubgraph"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - let Some((selected_left, selected_right)) = self.selected_vertices(config) else { - return false; - }; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let Some((selected_left, selected_right)) = self.selected_vertices(config) else { + return crate::types::Or(false); + }; - if selected_left.len() != self.k || selected_right.len() != self.k { - return false; - } + if selected_left.len() != self.k || selected_right.len() != self.k { + return crate::types::Or(false); + } - selected_left.iter().all(|&left| { - selected_right - .iter() - .all(|&right| self.has_selected_edge(left, right)) + selected_left.iter().all(|&left| { + selected_right + .iter() + .all(|&right| self.has_selected_edge(left, right)) + }) }) } @@ -129,8 +131,6 @@ impl Problem for BalancedCompleteBipartiteSubgraph { } } -impl SatisfactionProblem for BalancedCompleteBipartiteSubgraph {} - #[derive(Deserialize)] struct BalancedCompleteBipartiteSubgraphRepr { graph: BipartiteGraph, @@ -144,7 +144,7 @@ impl From for BalancedCompleteBipartiteSu } crate::declare_variants! { - default sat BalancedCompleteBipartiteSubgraph => "1.3803^num_vertices", + default BalancedCompleteBipartiteSubgraph => "1.3803^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/biclique_cover.rs b/src/models/graph/biclique_cover.rs index ca1bf2e4a..868649c02 100644 --- a/src/models/graph/biclique_cover.rs +++ b/src/models/graph/biclique_cover.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::BipartiteGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -45,7 +45,7 @@ inventory::submit! { /// let problem = BicliqueCover::new(graph, 2); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Check coverage /// for sol in &solutions { @@ -219,18 +219,18 @@ pub(crate) fn is_biclique_cover( impl Problem for BicliqueCover { const NAME: &'static str = "BicliqueCover"; - type Metric = SolutionSize; + type Value = Min; fn dims(&self) -> Vec { // Each vertex has k binary variables (one per biclique) vec![2; self.num_vertices() * self.k] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !self.is_valid_cover(config) { - return SolutionSize::Invalid; + return Min(None); } - SolutionSize::Valid(self.total_biclique_size(config) as i32) + Min(Some(self.total_biclique_size(config) as i32)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -238,16 +238,8 @@ impl Problem for BicliqueCover { } } -impl OptimizationProblem for BicliqueCover { - type Value = i32; - - fn direction(&self) -> Direction { - Direction::Minimize - } -} - crate::declare_variants! { - default opt BicliqueCover => "2^num_vertices", + default BicliqueCover => "2^num_vertices", } #[cfg(feature = "example-db")] @@ -260,7 +252,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -175,19 +175,14 @@ where vec![2; self.num_potential_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.augmented_graph(config) - .is_some_and(|graph| is_biconnected(&graph)) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + self.augmented_graph(config) + .is_some_and(|graph| is_biconnected(&graph)) + }) } } -impl SatisfactionProblem for BiconnectivityAugmentation -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ -} - fn normalize_edge(u: usize, v: usize) -> (usize, usize) { if u <= v { (u, v) @@ -260,7 +255,7 @@ fn is_biconnected(graph: &G) -> bool { } crate::declare_variants! { - default sat BiconnectivityAugmentation => "2^num_potential_edges", + default BiconnectivityAugmentation => "2^num_potential_edges", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/bottleneck_traveling_salesman.rs b/src/models/graph/bottleneck_traveling_salesman.rs index e86b7c95e..3badad9cb 100644 --- a/src/models/graph/bottleneck_traveling_salesman.rs +++ b/src/models/graph/bottleneck_traveling_salesman.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -98,7 +98,7 @@ impl BottleneckTravelingSalesman { impl Problem for BottleneckTravelingSalesman { const NAME: &'static str = "BottleneckTravelingSalesman"; - type Metric = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -108,14 +108,14 @@ impl Problem for BottleneckTravelingSalesman { vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if config.len() != self.graph.num_edges() { - return SolutionSize::Invalid; + return Min(None); } let selected: Vec = config.iter().map(|&s| s == 1).collect(); if !super::traveling_salesman::is_hamiltonian_cycle(&self.graph, &selected) { - return SolutionSize::Invalid; + return Min(None); } let bottleneck = config @@ -125,15 +125,7 @@ impl Problem for BottleneckTravelingSalesman { .max() .expect("valid Hamiltonian cycle selects at least one edge"); - SolutionSize::Valid(bottleneck) - } -} - -impl OptimizationProblem for BottleneckTravelingSalesman { - type Value = i32; - - fn direction(&self) -> Direction { - Direction::Minimize + Min(Some(bottleneck)) } } @@ -160,12 +152,12 @@ pub(crate) fn canonical_model_example_specs() -> Vec "num_vertices^2 * 2^num_vertices", + default BottleneckTravelingSalesman => "num_vertices^2 * 2^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/bounded_component_spanning_forest.rs b/src/models/graph/bounded_component_spanning_forest.rs index 053edbf57..32f229a1a 100644 --- a/src/models/graph/bounded_component_spanning_forest.rs +++ b/src/models/graph/bounded_component_spanning_forest.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -185,7 +185,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "BoundedComponentSpanningForest"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -195,18 +195,11 @@ where vec![self.max_components; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl SatisfactionProblem for BoundedComponentSpanningForest -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ -} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -237,7 +230,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "3^num_vertices", + default BoundedComponentSpanningForest => "3^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/directed_two_commodity_integral_flow.rs b/src/models/graph/directed_two_commodity_integral_flow.rs index 20970c62b..97b4dd49b 100644 --- a/src/models/graph/directed_two_commodity_integral_flow.rs +++ b/src/models/graph/directed_two_commodity_integral_flow.rs @@ -8,7 +8,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -64,7 +64,7 @@ inventory::submit! { /// graph, vec![1; 8], 0, 4, 1, 5, 1, 1, /// ); /// let solver = BruteForce::new(); -/// assert!(solver.find_satisfying(&problem).is_some()); +/// assert!(solver.find_witness(&problem).is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DirectedTwoCommodityIntegralFlow { @@ -244,7 +244,7 @@ impl DirectedTwoCommodityIntegralFlow { impl Problem for DirectedTwoCommodityIntegralFlow { const NAME: &'static str = "DirectedTwoCommodityIntegralFlow"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { self.capacities @@ -254,8 +254,8 @@ impl Problem for DirectedTwoCommodityIntegralFlow { .collect() } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_feasible(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_feasible(config)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -263,10 +263,8 @@ impl Problem for DirectedTwoCommodityIntegralFlow { } } -impl SatisfactionProblem for DirectedTwoCommodityIntegralFlow {} - crate::declare_variants! { - default sat DirectedTwoCommodityIntegralFlow => "(max_capacity + 1)^(2 * num_arcs)", + default DirectedTwoCommodityIntegralFlow => "(max_capacity + 1)^(2 * num_arcs)", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/disjoint_connecting_paths.rs b/src/models/graph/disjoint_connecting_paths.rs index 2f6bcebd6..d565fa123 100644 --- a/src/models/graph/disjoint_connecting_paths.rs +++ b/src/models/graph/disjoint_connecting_paths.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; @@ -117,7 +117,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "DisjointConnectingPaths"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -127,13 +127,11 @@ where vec![2; self.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl SatisfactionProblem for DisjointConnectingPaths {} - fn canonical_edges(graph: &G) -> Vec<(usize, usize)> { let mut edges = graph .edges() @@ -245,7 +243,7 @@ fn is_valid_disjoint_connecting_paths( } crate::declare_variants! { - default sat DisjointConnectingPaths => "2^num_edges", + default DisjointConnectingPaths => "2^num_edges", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/generalized_hex.rs b/src/models/graph/generalized_hex.rs index 29b6c870c..ef7252aae 100644 --- a/src/models/graph/generalized_hex.rs +++ b/src/models/graph/generalized_hex.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::variant::VariantParam; inventory::submit! { @@ -239,7 +239,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "GeneralizedHex"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -249,22 +249,22 @@ where vec![] } - fn evaluate(&self, config: &[usize]) -> bool { - if !config.is_empty() { - return false; - } - let playable_vertices = self.playable_vertices(); - let vertex_to_state_index = self.vertex_to_state_index(&playable_vertices); - let mut state = vec![ClaimState::Unclaimed; playable_vertices.len()]; - let mut memo = HashMap::new(); - self.first_player_wins(&mut state, &vertex_to_state_index, &mut memo) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if !config.is_empty() { + return crate::types::Or(false); + } + let playable_vertices = self.playable_vertices(); + let vertex_to_state_index = self.vertex_to_state_index(&playable_vertices); + let mut state = vec![ClaimState::Unclaimed; playable_vertices.len()]; + let mut memo = HashMap::new(); + self.first_player_wins(&mut state, &vertex_to_state_index, &mut memo) + }) } } -impl SatisfactionProblem for GeneralizedHex where G: Graph + VariantParam {} - crate::declare_variants! { - default sat GeneralizedHex => "3^num_playable_vertices", + default GeneralizedHex => "3^num_playable_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/graph_partitioning.rs b/src/models/graph/graph_partitioning.rs index 1ace66b80..a2ea1f0f6 100644 --- a/src/models/graph/graph_partitioning.rs +++ b/src/models/graph/graph_partitioning.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -40,7 +40,7 @@ inventory::submit! { /// ``` /// use problemreductions::models::graph::GraphPartitioning; /// use problemreductions::topology::SimpleGraph; -/// use problemreductions::types::SolutionSize; +/// use problemreductions::types::Min; /// use problemreductions::{Problem, Solver, BruteForce}; /// /// // Square graph: 0-1, 1-2, 2-3, 3-0 @@ -48,12 +48,12 @@ inventory::submit! { /// let problem = GraphPartitioning::new(graph); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Minimum bisection of a 4-cycle: cut = 2 /// for sol in solutions { /// let size = problem.evaluate(&sol); -/// assert_eq!(size, SolutionSize::Valid(2)); +/// assert_eq!(size, Min(Some(2))); /// } /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -92,7 +92,7 @@ where G: Graph + crate::variant::VariantParam, { const NAME: &'static str = "GraphPartitioning"; - type Metric = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -102,19 +102,19 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let n = self.graph.num_vertices(); if config.len() != n { - return SolutionSize::Invalid; + return Min(None); } // Balanced bisection requires even n if !n.is_multiple_of(2) { - return SolutionSize::Invalid; + return Min(None); } // Check balanced: exactly n/2 vertices in partition 1 let count_ones = config.iter().filter(|&&x| x == 1).count(); if count_ones != n / 2 { - return SolutionSize::Invalid; + return Min(None); } // Count crossing edges let mut cut = 0i32; @@ -123,23 +123,12 @@ where cut += 1; } } - SolutionSize::Valid(cut) - } -} - -impl OptimizationProblem for GraphPartitioning -where - G: Graph + crate::variant::VariantParam, -{ - type Value = i32; - - fn direction(&self) -> Direction { - Direction::Minimize + Min(Some(cut)) } } crate::declare_variants! { - default opt GraphPartitioning => "2^num_vertices", + default GraphPartitioning => "2^num_vertices", } #[cfg(feature = "example-db")] @@ -163,7 +152,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -103,8 +103,8 @@ where vec![n; n] } - fn evaluate(&self, config: &[usize]) -> bool { - is_valid_hamiltonian_circuit(&self.graph, config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(is_valid_hamiltonian_circuit(&self.graph, config)) } } @@ -140,8 +140,6 @@ pub(crate) fn is_valid_hamiltonian_circuit(graph: &G, config: &[usize] true } -impl SatisfactionProblem for HamiltonianCircuit {} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -167,7 +165,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "1.657^num_vertices", + default HamiltonianCircuit => "1.657^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/hamiltonian_path.rs b/src/models/graph/hamiltonian_path.rs index 0ab9ba528..ddc39ffa1 100644 --- a/src/models/graph/hamiltonian_path.rs +++ b/src/models/graph/hamiltonian_path.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -58,7 +58,7 @@ inventory::submit! { /// let problem = HamiltonianPath::new(graph); /// /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -99,7 +99,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "HamiltonianPath"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -110,13 +110,11 @@ where vec![n; n] } - fn evaluate(&self, config: &[usize]) -> bool { - is_valid_hamiltonian_path(&self.graph, config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(is_valid_hamiltonian_path(&self.graph, config)) } } -impl SatisfactionProblem for HamiltonianPath {} - /// Check if a configuration represents a valid Hamiltonian path in the graph. /// /// A valid Hamiltonian path is a permutation of the vertices such that @@ -170,7 +168,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "1.657^num_vertices", + default HamiltonianPath => "1.657^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/integral_flow_bundles.rs b/src/models/graph/integral_flow_bundles.rs index 383c2171d..893cb1da3 100644 --- a/src/models/graph/integral_flow_bundles.rs +++ b/src/models/graph/integral_flow_bundles.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; @@ -172,7 +172,7 @@ impl IntegralFlowBundles { /// Check whether a configuration is feasible. pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } fn arc_upper_bounds(&self) -> Vec { @@ -202,7 +202,7 @@ impl IntegralFlowBundles { impl Problem for IntegralFlowBundles { const NAME: &'static str = "IntegralFlowBundles"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { self.arc_upper_bounds() @@ -216,47 +216,49 @@ impl Problem for IntegralFlowBundles { .collect() } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_arcs() { - return false; - } - - let upper_bounds = self.arc_upper_bounds(); - for (&value, &upper_bound) in config.iter().zip(&upper_bounds) { - if u64::try_from(value).map_or(true, |value| value > upper_bound) { - return false; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_arcs() { + return crate::types::Or(false); } - } - for (bundle, &capacity) in self.bundles.iter().zip(&self.bundle_capacities) { - let mut total = 0u64; - for &arc_index in bundle { - let Ok(flow) = u64::try_from(config[arc_index]) else { - return false; - }; - let Some(next_total) = total.checked_add(flow) else { - return false; - }; - total = next_total; + let upper_bounds = self.arc_upper_bounds(); + for (&value, &upper_bound) in config.iter().zip(&upper_bounds) { + if u64::try_from(value).map_or(true, |value| value > upper_bound) { + return crate::types::Or(false); + } } - if total > capacity { - return false; - } - } - for vertex in 0..self.num_vertices() { - if vertex == self.source || vertex == self.sink { - continue; + for (bundle, &capacity) in self.bundles.iter().zip(&self.bundle_capacities) { + let mut total = 0u64; + for &arc_index in bundle { + let Ok(flow) = u64::try_from(config[arc_index]) else { + return crate::types::Or(false); + }; + let Some(next_total) = total.checked_add(flow) else { + return crate::types::Or(false); + }; + total = next_total; + } + if total > capacity { + return crate::types::Or(false); + } } - if self.vertex_balance(config, vertex) != Some(0) { - return false; + + for vertex in 0..self.num_vertices() { + if vertex == self.source || vertex == self.sink { + continue; + } + if self.vertex_balance(config, vertex) != Some(0) { + return crate::types::Or(false); + } } - } - matches!( - self.vertex_balance(config, self.sink), - Some(balance) if balance >= i128::from(self.requirement) - ) + matches!( + self.vertex_balance(config, self.sink), + Some(balance) if balance >= i128::from(self.requirement) + ) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -264,10 +266,8 @@ impl Problem for IntegralFlowBundles { } } -impl SatisfactionProblem for IntegralFlowBundles {} - crate::declare_variants! { - default sat IntegralFlowBundles => "2^num_arcs", + default IntegralFlowBundles => "2^num_arcs", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/integral_flow_homologous_arcs.rs b/src/models/graph/integral_flow_homologous_arcs.rs index 51ae2b0cd..0798cd834 100644 --- a/src/models/graph/integral_flow_homologous_arcs.rs +++ b/src/models/graph/integral_flow_homologous_arcs.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -139,7 +139,7 @@ impl IntegralFlowHomologousArcs { } pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } fn domain_size(capacity: u64) -> usize { @@ -152,7 +152,7 @@ impl IntegralFlowHomologousArcs { impl Problem for IntegralFlowHomologousArcs { const NAME: &'static str = "IntegralFlowHomologousArcs"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -165,50 +165,50 @@ impl Problem for IntegralFlowHomologousArcs { .collect() } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_arcs() { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_arcs() { + return crate::types::Or(false); + } - for &(a, b) in &self.homologous_pairs { - if config[a] != config[b] { - return false; + for &(a, b) in &self.homologous_pairs { + if config[a] != config[b] { + return crate::types::Or(false); + } } - } - let mut balances = vec![0_i128; self.num_vertices()]; - for (arc_index, ((u, v), &capacity)) in self - .graph - .arcs() - .into_iter() - .zip(self.capacities.iter()) - .enumerate() - { - let Ok(flow) = u64::try_from(config[arc_index]) else { - return false; - }; - if flow > capacity { - return false; + let mut balances = vec![0_i128; self.num_vertices()]; + for (arc_index, ((u, v), &capacity)) in self + .graph + .arcs() + .into_iter() + .zip(self.capacities.iter()) + .enumerate() + { + let Ok(flow) = u64::try_from(config[arc_index]) else { + return crate::types::Or(false); + }; + if flow > capacity { + return crate::types::Or(false); + } + let flow = i128::from(flow); + balances[u] -= flow; + balances[v] += flow; } - let flow = i128::from(flow); - balances[u] -= flow; - balances[v] += flow; - } - for (vertex, &balance) in balances.iter().enumerate() { - if vertex != self.source && vertex != self.sink && balance != 0 { - return false; + for (vertex, &balance) in balances.iter().enumerate() { + if vertex != self.source && vertex != self.sink && balance != 0 { + return crate::types::Or(false); + } } - } - balances[self.sink] >= i128::from(self.requirement) + balances[self.sink] >= i128::from(self.requirement) + }) } } -impl SatisfactionProblem for IntegralFlowHomologousArcs {} - crate::declare_variants! { - default sat IntegralFlowHomologousArcs => "(max_capacity + 1)^num_arcs", + default IntegralFlowHomologousArcs => "(max_capacity + 1)^num_arcs", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/integral_flow_with_multipliers.rs b/src/models/graph/integral_flow_with_multipliers.rs index 4f25d731e..d620d4b24 100644 --- a/src/models/graph/integral_flow_with_multipliers.rs +++ b/src/models/graph/integral_flow_with_multipliers.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -195,7 +195,7 @@ impl IntegralFlowWithMultipliers { impl Problem for IntegralFlowWithMultipliers { const NAME: &'static str = "IntegralFlowWithMultipliers"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { self.capacities @@ -204,8 +204,8 @@ impl Problem for IntegralFlowWithMultipliers { .collect() } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_feasible(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_feasible(config)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -213,10 +213,8 @@ impl Problem for IntegralFlowWithMultipliers { } } -impl SatisfactionProblem for IntegralFlowWithMultipliers {} - crate::declare_variants! { - default sat IntegralFlowWithMultipliers => "(max_capacity + 1)^num_arcs", + default IntegralFlowWithMultipliers => "(max_capacity + 1)^num_arcs", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/isomorphic_spanning_tree.rs b/src/models/graph/isomorphic_spanning_tree.rs index 8e3a55075..b340b6c59 100644 --- a/src/models/graph/isomorphic_spanning_tree.rs +++ b/src/models/graph/isomorphic_spanning_tree.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -44,7 +44,7 @@ inventory::submit! { /// let problem = IsomorphicSpanningTree::new(graph, tree); /// /// let solver = BruteForce::new(); -/// let sol = solver.find_satisfying(&problem); +/// let sol = solver.find_witness(&problem); /// assert!(sol.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -125,37 +125,39 @@ impl IsomorphicSpanningTree { impl Problem for IsomorphicSpanningTree { const NAME: &'static str = "IsomorphicSpanningTree"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { let n = self.graph.num_vertices(); vec![n; n] } - fn evaluate(&self, config: &[usize]) -> bool { - let n = self.graph.num_vertices(); - if config.len() != n { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n = self.graph.num_vertices(); + if config.len() != n { + return crate::types::Or(false); + } - // Check that config is a valid permutation: all values in 0..n, all distinct - let mut seen = vec![false; n]; - for &v in config { - if v >= n || seen[v] { - return false; + // Check that config is a valid permutation: all values in 0..n, all distinct + let mut seen = vec![false; n]; + for &v in config { + if v >= n || seen[v] { + return crate::types::Or(false); + } + seen[v] = true; } - seen[v] = true; - } - // Check that every tree edge maps to a graph edge under the permutation - // config[i] = π(i): tree vertex i maps to graph vertex config[i] - for (u, v) in self.tree.edges() { - if !self.graph.has_edge(config[u], config[v]) { - return false; + // Check that every tree edge maps to a graph edge under the permutation + // config[i] = π(i): tree vertex i maps to graph vertex config[i] + for (u, v) in self.tree.edges() { + if !self.graph.has_edge(config[u], config[v]) { + return crate::types::Or(false); + } } - } - true + true + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -163,8 +165,6 @@ impl Problem for IsomorphicSpanningTree { } } -impl SatisfactionProblem for IsomorphicSpanningTree {} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -179,7 +179,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec "factorial(num_vertices)", + default IsomorphicSpanningTree => "factorial(num_vertices)", } #[cfg(test)] diff --git a/src/models/graph/kclique.rs b/src/models/graph/kclique.rs index d16e2911c..d0b84cbcf 100644 --- a/src/models/graph/kclique.rs +++ b/src/models/graph/kclique.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -73,7 +73,7 @@ where G: Graph + crate::variant::VariantParam, { const NAME: &'static str = "KClique"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -83,13 +83,11 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - is_kclique_config(&self.graph, config, self.k) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(is_kclique_config(&self.graph, config, self.k)) } } -impl SatisfactionProblem for KClique where G: Graph + crate::variant::VariantParam {} - fn is_kclique_config(graph: &G, config: &[usize], k: usize) -> bool { if config.len() != graph.num_vertices() { return false; @@ -124,7 +122,7 @@ fn is_kclique_config(graph: &G, config: &[usize], k: usize) -> bool { } crate::declare_variants! { - default sat KClique => "1.1996^num_vertices", + default KClique => "1.1996^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/kcoloring.rs b/src/models/graph/kcoloring.rs index 195cbbff0..9e810dfc9 100644 --- a/src/models/graph/kcoloring.rs +++ b/src/models/graph/kcoloring.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::variant::{KValue, VariantParam, K2, K3, K4, K5, KN}; use serde::{Deserialize, Serialize}; @@ -49,7 +49,7 @@ inventory::submit! { /// let problem = KColoring::::new(graph); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_satisfying(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Verify all solutions are valid colorings /// for sol in &solutions { @@ -145,7 +145,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "KColoring"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![K, G] @@ -155,13 +155,11 @@ where vec![self.num_colors; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_coloring(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_coloring(config)) } } -impl SatisfactionProblem for KColoring {} - /// Check if a coloring is valid for a graph. /// /// # Panics @@ -203,12 +201,12 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "2^num_vertices", - sat KColoring => "num_vertices + num_edges", - sat KColoring => "1.3289^num_vertices", - sat KColoring => "1.7159^num_vertices", + default KColoring => "2^num_vertices", + KColoring => "num_vertices + num_edges", + KColoring => "1.3289^num_vertices", + KColoring => "1.7159^num_vertices", // Best known: O*((2-ε)^n) for some ε > 0 (Zamir 2021), concrete ε unknown - sat KColoring => "2^num_vertices", + KColoring => "2^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/kth_best_spanning_tree.rs b/src/models/graph/kth_best_spanning_tree.rs index d65e86441..f26c26fcb 100644 --- a/src/models/graph/kth_best_spanning_tree.rs +++ b/src/models/graph/kth_best_spanning_tree.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -207,7 +207,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "KthBestSpanningTree"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -217,16 +217,11 @@ where vec![2; self.k * self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.evaluate_config(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.evaluate_config(config)) } } -impl SatisfactionProblem for KthBestSpanningTree where - W: WeightElement + crate::variant::VariantParam -{ -} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { // K4 with weights [1,1,2,2,2,3], k=2, B=4. @@ -245,7 +240,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "2^(num_edges * k)", + default KthBestSpanningTree => "2^(num_edges * k)", } #[cfg(test)] diff --git a/src/models/graph/length_bounded_disjoint_paths.rs b/src/models/graph/length_bounded_disjoint_paths.rs index 6540f11c6..339ec25eb 100644 --- a/src/models/graph/length_bounded_disjoint_paths.rs +++ b/src/models/graph/length_bounded_disjoint_paths.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -135,7 +135,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "LengthBoundedDisjointPaths"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -145,13 +145,11 @@ where vec![2; self.num_paths_required * self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl SatisfactionProblem for LengthBoundedDisjointPaths {} - fn is_valid_path_collection( graph: &G, source: usize, @@ -315,7 +313,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "2^(num_paths_required * num_vertices)", + default LengthBoundedDisjointPaths => "2^(num_paths_required * num_vertices)", } #[cfg(test)] diff --git a/src/models/graph/longest_circuit.rs b/src/models/graph/longest_circuit.rs index 189645745..db22d76fd 100644 --- a/src/models/graph/longest_circuit.rs +++ b/src/models/graph/longest_circuit.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -161,7 +161,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "LongestCircuit"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -171,8 +171,8 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } @@ -242,13 +242,6 @@ pub(crate) fn is_simple_circuit(graph: &G, config: &[usize]) -> bool { visited_selected_vertices == selected_vertices.len() } -impl SatisfactionProblem for LongestCircuit -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ -} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -278,7 +271,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "2^num_vertices * num_vertices^2", + default LongestCircuit => "2^num_vertices * num_vertices^2", } #[cfg(test)] diff --git a/src/models/graph/longest_path.rs b/src/models/graph/longest_path.rs index 682de7ed8..fd70dbeb3 100644 --- a/src/models/graph/longest_path.rs +++ b/src/models/graph/longest_path.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, One, SolutionSize, WeightElement}; +use crate::traits::Problem; +use crate::types::{Max, One, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; @@ -150,7 +150,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "LongestPath"; - type Metric = SolutionSize; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -160,9 +160,9 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if !self.is_valid_solution(config) { - return SolutionSize::Invalid; + return Max(None); } let mut total = W::Sum::zero(); @@ -171,19 +171,7 @@ where total += self.edge_lengths[idx].to_sum(); } } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for LongestPath -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Maximize + Max(Some(total)) } } @@ -265,8 +253,8 @@ fn is_simple_st_path( } crate::declare_variants! { - default opt LongestPath => "num_vertices * 2^num_vertices", - opt LongestPath => "num_vertices * 2^num_vertices", + default LongestPath => "num_vertices * 2^num_vertices", + LongestPath => "num_vertices * 2^num_vertices", } #[cfg(feature = "example-db")] @@ -294,7 +282,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -181,22 +181,10 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { // All cuts are valid, so always return Valid let partition: Vec = config.iter().map(|&c| c != 0).collect(); - SolutionSize::Valid(cut_size(&self.graph, &self.edge_weights, &partition)) - } -} - -impl OptimizationProblem for MaxCut -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Maximize + Max(Some(cut_size(&self.graph, &self.edge_weights, &partition))) } } @@ -221,7 +209,7 @@ where } crate::declare_variants! { - default opt MaxCut => "2^(2.372 * num_vertices / 3)", + default MaxCut => "2^(2.372 * num_vertices / 3)", } #[cfg(feature = "example-db")] @@ -233,7 +221,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -163,9 +163,9 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if !self.is_maximal(config) { - return SolutionSize::Invalid; + return Max(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -173,19 +173,7 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for MaximalIS -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Maximize + Max(Some(total)) } } @@ -198,7 +186,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec(graph: &G, selected: &[bool]) } crate::declare_variants! { - default opt MaximalIS => "3^(num_vertices / 3)", + default MaximalIS => "3^(num_vertices / 3)", } #[cfg(test)] diff --git a/src/models/graph/maximum_clique.rs b/src/models/graph/maximum_clique.rs index 8cf23de0c..6dd4cb284 100644 --- a/src/models/graph/maximum_clique.rs +++ b/src/models/graph/maximum_clique.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize, WeightElement}; +use crate::traits::Problem; +use crate::types::{Max, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -53,7 +53,7 @@ inventory::submit! { /// /// // Solve with brute force /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Maximum clique in a triangle (K3) is size 3 /// assert!(solutions.iter().all(|s| s.iter().sum::() == 3)); @@ -119,7 +119,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MaximumClique"; - type Metric = SolutionSize; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -129,9 +129,9 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if !is_clique_config(&self.graph, config) { - return SolutionSize::Invalid; + return Max(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -139,19 +139,7 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for MaximumClique -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Maximize + Max(Some(total)) } } @@ -177,7 +165,7 @@ fn is_clique_config(graph: &G, config: &[usize]) -> bool { } crate::declare_variants! { - default opt MaximumClique => "1.1996^num_vertices", + default MaximumClique => "1.1996^num_vertices", } #[cfg(feature = "example-db")] @@ -189,7 +177,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec() == 1)); @@ -119,7 +119,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MaximumIndependentSet"; - type Metric = SolutionSize; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -129,9 +129,9 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if !is_independent_set_config(&self.graph, config) { - return SolutionSize::Invalid; + return Max(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -139,19 +139,7 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for MaximumIndependentSet -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Maximize + Max(Some(total)) } } @@ -166,13 +154,13 @@ fn is_independent_set_config(graph: &G, config: &[usize]) -> bool { } crate::declare_variants! { - opt MaximumIndependentSet => "1.1996^num_vertices", - default opt MaximumIndependentSet => "1.1996^num_vertices", - opt MaximumIndependentSet => "2^sqrt(num_vertices)", - opt MaximumIndependentSet => "2^sqrt(num_vertices)", - opt MaximumIndependentSet => "2^sqrt(num_vertices)", - opt MaximumIndependentSet => "2^sqrt(num_vertices)", - opt MaximumIndependentSet => "2^sqrt(num_vertices)", + MaximumIndependentSet => "1.1996^num_vertices", + default MaximumIndependentSet => "1.1996^num_vertices", + MaximumIndependentSet => "2^sqrt(num_vertices)", + MaximumIndependentSet => "2^sqrt(num_vertices)", + MaximumIndependentSet => "2^sqrt(num_vertices)", + MaximumIndependentSet => "2^sqrt(num_vertices)", + MaximumIndependentSet => "2^sqrt(num_vertices)", } #[cfg(feature = "example-db")] @@ -204,7 +192,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec::unit_weights(graph); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Maximum matching has 1 edge /// for sol in &solutions { @@ -187,7 +187,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MaximumMatching"; - type Metric = SolutionSize; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -197,9 +197,9 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if !self.is_valid_matching(config) { - return SolutionSize::Invalid; + return Max(None); } let mut total = W::Sum::zero(); for (idx, &selected) in config.iter().enumerate() { @@ -209,24 +209,12 @@ where } } } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for MaximumMatching -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Maximize + Max(Some(total)) } } crate::declare_variants! { - default opt MaximumMatching => "num_vertices^3", + default MaximumMatching => "num_vertices^3", } #[cfg(feature = "example-db")] @@ -238,7 +226,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -256,46 +256,44 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.graph.num_vertices() || config.iter().any(|&selected| selected > 1) - { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.graph.num_vertices() + || config.iter().any(|&selected| selected > 1) + { + return crate::types::Or(false); + } - // Check exactly K centers are selected - let num_selected = config.iter().filter(|&&selected| selected == 1).count(); - if num_selected != self.k { - return false; - } + // Check exactly K centers are selected + let num_selected = config.iter().filter(|&&selected| selected == 1).count(); + if num_selected != self.k { + return crate::types::Or(false); + } - // Compute shortest distances to nearest center - let distances = match self.shortest_distances(config) { - Some(d) => d, - None => return false, - }; + // Compute shortest distances to nearest center + let distances = match self.shortest_distances(config) { + Some(d) => d, + None => { + return crate::types::Or(false); + } + }; - // Compute max weighted distance: max_{v} w(v) * d(v) - let mut max_wd = W::Sum::zero(); - for (v, dist) in distances.iter().enumerate() { - let wd = self.vertex_weights[v].to_sum() * dist.clone(); - if wd > max_wd { - max_wd = wd; + // Compute max weighted distance: max_{v} w(v) * d(v) + let mut max_wd = W::Sum::zero(); + for (v, dist) in distances.iter().enumerate() { + let wd = self.vertex_weights[v].to_sum() * dist.clone(); + if wd > max_wd { + max_wd = wd; + } } - } - max_wd <= self.bound + max_wd <= self.bound + }) } } -impl SatisfactionProblem for MinMaxMulticenter -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ -} - crate::declare_variants! { - default sat MinMaxMulticenter => "1.4969^num_vertices", + default MinMaxMulticenter => "1.4969^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/minimum_cut_into_bounded_sets.rs b/src/models/graph/minimum_cut_into_bounded_sets.rs index 6523ed5aa..ed221d11b 100644 --- a/src/models/graph/minimum_cut_into_bounded_sets.rs +++ b/src/models/graph/minimum_cut_into_bounded_sets.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -164,7 +164,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinimumCutIntoBoundedSets"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -174,47 +174,42 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - let n = self.graph.num_vertices(); - if config.len() != n { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n = self.graph.num_vertices(); + if config.len() != n { + return crate::types::Or(false); + } - // Check source is in V1 (config=0) and sink is in V2 (config=1) - if config[self.source] != 0 { - return false; - } - if config[self.sink] != 1 { - return false; - } + // Check source is in V1 (config=0) and sink is in V2 (config=1) + if config[self.source] != 0 { + return crate::types::Or(false); + } + if config[self.sink] != 1 { + return crate::types::Or(false); + } - // Check size bounds - let count_v1 = config.iter().filter(|&&x| x == 0).count(); - let count_v2 = config.iter().filter(|&&x| x == 1).count(); - if count_v1 > self.size_bound || count_v2 > self.size_bound { - return false; - } + // Check size bounds + let count_v1 = config.iter().filter(|&&x| x == 0).count(); + let count_v2 = config.iter().filter(|&&x| x == 1).count(); + if count_v1 > self.size_bound || count_v2 > self.size_bound { + return crate::types::Or(false); + } - // Compute cut weight - let mut cut_weight = W::Sum::zero(); - for ((u, v), weight) in self.graph.edges().iter().zip(self.edge_weights.iter()) { - if config[*u] != config[*v] { - cut_weight += weight.to_sum(); + // Compute cut weight + let mut cut_weight = W::Sum::zero(); + for ((u, v), weight) in self.graph.edges().iter().zip(self.edge_weights.iter()) { + if config[*u] != config[*v] { + cut_weight += weight.to_sum(); + } } - } - // Check cut weight <= K - cut_weight <= self.cut_bound + // Check cut weight <= K + cut_weight <= self.cut_bound + }) } } -impl SatisfactionProblem for MinimumCutIntoBoundedSets -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ -} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -250,7 +245,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "2^num_vertices", + default MinimumCutIntoBoundedSets => "2^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/minimum_dominating_set.rs b/src/models/graph/minimum_dominating_set.rs index fd7c4aeb3..8b3f69e52 100644 --- a/src/models/graph/minimum_dominating_set.rs +++ b/src/models/graph/minimum_dominating_set.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -48,7 +48,7 @@ inventory::submit! { /// let problem = MinimumDominatingSet::new(graph, vec![1; 4]); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Minimum dominating set is just the center vertex /// assert!(solutions.contains(&vec![1, 0, 0, 0])); @@ -139,7 +139,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinimumDominatingSet"; - type Metric = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -149,9 +149,9 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !self.is_dominating(config) { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -159,24 +159,12 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for MinimumDominatingSet -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Minimize + Min(Some(total)) } } crate::declare_variants! { - default opt MinimumDominatingSet => "1.4969^num_vertices", + default MinimumDominatingSet => "1.4969^num_vertices", } #[cfg(feature = "example-db")] @@ -188,7 +176,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -174,9 +174,9 @@ impl Problem for MinimumDummyActivitiesPert { vec![2; self.graph.num_arcs()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let Some(candidate) = self.build_candidate_network(config) else { - return SolutionSize::Invalid; + return Min(None); }; let source_reachability = reachability_matrix(&self.graph); @@ -189,27 +189,19 @@ impl Problem for MinimumDummyActivitiesPert { || event_reachability[candidate.finish_events[source]] [candidate.start_events[target]]; if source_reachability[source][target] != pert_reachable { - return SolutionSize::Invalid; + return Min(None); } } } - SolutionSize::Valid( + Min(Some( i32::try_from(candidate.num_dummy_arcs).expect("dummy activity count must fit in i32"), - ) - } -} - -impl OptimizationProblem for MinimumDummyActivitiesPert { - type Value = i32; - - fn direction(&self) -> Direction { - Direction::Minimize + )) } } crate::declare_variants! { - default opt MinimumDummyActivitiesPert => "2^num_arcs", + default MinimumDummyActivitiesPert => "2^num_arcs", } #[cfg(feature = "example-db")] @@ -221,7 +213,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec(), 1); @@ -126,7 +126,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinimumFeedbackArcSet"; - type Metric = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -136,9 +136,9 @@ where vec![2; self.graph.num_arcs()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !is_valid_fas(&self.graph, config) { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -146,18 +146,7 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for MinimumFeedbackArcSet -where - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Minimize + Min(Some(total)) } } @@ -176,7 +165,7 @@ fn is_valid_fas(graph: &DirectedGraph, config: &[usize]) -> bool { } crate::declare_variants! { - default opt MinimumFeedbackArcSet => "2^num_vertices", + default MinimumFeedbackArcSet => "2^num_vertices", } #[cfg(feature = "example-db")] @@ -190,7 +179,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -132,15 +132,15 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if config.len() != self.graph.num_vertices() { - return SolutionSize::Invalid; + return Min(None); } // keep[v] = true if vertex v is NOT selected for removal let keep: Vec = config.iter().map(|&c| c == 0).collect(); let subgraph = self.graph.induced_subgraph(&keep); if !subgraph.is_dag() { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -148,23 +148,12 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for MinimumFeedbackVertexSet -where - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Minimize + Min(Some(total)) } } crate::declare_variants! { - default opt MinimumFeedbackVertexSet => "1.9977^num_vertices", + default MinimumFeedbackVertexSet => "1.9977^num_vertices", } #[cfg(feature = "example-db")] @@ -180,7 +169,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -171,9 +171,9 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !terminals_separated(&self.graph, &self.terminals, config) { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (idx, &selected) in config.iter().enumerate() { @@ -183,24 +183,12 @@ where } } } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for MinimumMultiwayCut -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Minimize + Min(Some(total)) } } crate::declare_variants! { - default opt MinimumMultiwayCut => "1.84^num_terminals * num_vertices^3", + default MinimumMultiwayCut => "1.84^num_terminals * num_vertices^3", } #[cfg(feature = "example-db")] @@ -213,7 +201,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -224,17 +224,17 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { // Check exactly K centers are selected let num_selected: usize = config.iter().sum(); if num_selected != self.k { - return SolutionSize::Invalid; + return Min(None); } // Compute shortest distances to nearest center let distances = match self.shortest_distances(config) { Some(d) => d, - None => return SolutionSize::Invalid, + None => return Min(None), }; // Compute total weighted distance: Σ w(v) * d(v) @@ -243,24 +243,12 @@ where total += self.vertex_weights[v].to_sum() * dist.clone(); } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for MinimumSumMulticenter -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Minimize + Min(Some(total)) } } crate::declare_variants! { - default opt MinimumSumMulticenter => "2^num_vertices", + default MinimumSumMulticenter => "2^num_vertices", } #[cfg(feature = "example-db")] @@ -286,7 +274,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -124,9 +124,9 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !is_vertex_cover_config(&self.graph, config) { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -134,19 +134,7 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for MinimumVertexCover -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Minimize + Min(Some(total)) } } @@ -163,7 +151,7 @@ fn is_vertex_cover_config(graph: &G, config: &[usize]) -> bool { } crate::declare_variants! { - default opt MinimumVertexCover => "1.1996^num_vertices", + default MinimumVertexCover => "1.1996^num_vertices", } #[cfg(feature = "example-db")] @@ -175,7 +163,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec bool { - self.evaluate(config) + self.evaluate(config).0 } } @@ -218,7 +218,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MixedChinesePostman"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -228,42 +228,41 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - let Some(oriented_pairs) = self.oriented_arc_pairs(config) else { - return false; - }; - - // Connectivity uses the full available graph: original arcs plus both - // directions of every undirected edge. - if !DirectedGraph::new(self.graph.num_vertices(), self.available_arc_pairs()) - .is_strongly_connected() - { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let Some(oriented_pairs) = self.oriented_arc_pairs(config) else { + return crate::types::Or(false); + }; + + // Connectivity uses the full available graph: original arcs plus both + // directions of every undirected edge. + if !DirectedGraph::new(self.graph.num_vertices(), self.available_arc_pairs()) + .is_strongly_connected() + { + return crate::types::Or(false); + } - // Shortest paths also use the full available graph so that balancing - // can route through undirected edges in either direction. - let distances = - all_pairs_shortest_paths(self.graph.num_vertices(), &self.weighted_available_arcs()); - // Degree imbalance is computed from the required arcs only (original - // arcs plus the chosen orientation of each undirected edge). - let balance = degree_imbalances(self.graph.num_vertices(), &oriented_pairs); - let Some(extra_cost) = minimum_balancing_cost(&balance, &distances) else { - return false; - }; - - self.base_cost() + extra_cost <= i64::from(self.bound) + // Shortest paths also use the full available graph so that balancing + // can route through undirected edges in either direction. + let distances = all_pairs_shortest_paths( + self.graph.num_vertices(), + &self.weighted_available_arcs(), + ); + // Degree imbalance is computed from the required arcs only (original + // arcs plus the chosen orientation of each undirected edge). + let balance = degree_imbalances(self.graph.num_vertices(), &oriented_pairs); + let Some(extra_cost) = minimum_balancing_cost(&balance, &distances) else { + return crate::types::Or(false); + }; + + self.base_cost() + extra_cost <= i64::from(self.bound) + }) } } -impl SatisfactionProblem for MixedChinesePostman where - W: WeightElement + crate::variant::VariantParam -{ -} - crate::declare_variants! { - default sat MixedChinesePostman => "2^num_edges * num_vertices^3", - sat MixedChinesePostman => "2^num_edges * num_vertices^3", + default MixedChinesePostman => "2^num_edges * num_vertices^3", + MixedChinesePostman => "2^num_edges * num_vertices^3", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/multiple_choice_branching.rs b/src/models/graph/multiple_choice_branching.rs index f7252ee3d..c0e90cdba 100644 --- a/src/models/graph/multiple_choice_branching.rs +++ b/src/models/graph/multiple_choice_branching.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::de::Error as _; @@ -176,7 +176,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MultipleChoiceBranching"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -186,22 +186,19 @@ where vec![2; self.graph.num_arcs()] } - fn evaluate(&self, config: &[usize]) -> bool { - is_valid_multiple_choice_branching( - &self.graph, - &self.weights, - &self.partition, - &self.threshold, - config, - ) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + is_valid_multiple_choice_branching( + &self.graph, + &self.weights, + &self.partition, + &self.threshold, + config, + ) + }) } } -impl SatisfactionProblem for MultipleChoiceBranching where - W: WeightElement + crate::variant::VariantParam -{ -} - fn validate_partition(partition: &[Vec], num_arcs: usize) { if let Some(message) = partition_validation_error(partition, num_arcs) { panic!("{message}"); @@ -297,7 +294,7 @@ fn is_valid_multiple_choice_branching( } crate::declare_variants! { - default sat MultipleChoiceBranching => "2^num_arcs", + default MultipleChoiceBranching => "2^num_arcs", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/multiple_copy_file_allocation.rs b/src/models/graph/multiple_copy_file_allocation.rs index 702ff1572..a2ef8cd79 100644 --- a/src/models/graph/multiple_copy_file_allocation.rs +++ b/src/models/graph/multiple_copy_file_allocation.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; @@ -179,7 +179,7 @@ impl MultipleCopyFileAllocation { impl Problem for MultipleCopyFileAllocation { const NAME: &'static str = "MultipleCopyFileAllocation"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -189,13 +189,11 @@ impl Problem for MultipleCopyFileAllocation { vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl SatisfactionProblem for MultipleCopyFileAllocation {} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -212,7 +210,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec "2^num_vertices", + default MultipleCopyFileAllocation => "2^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/optimal_linear_arrangement.rs b/src/models/graph/optimal_linear_arrangement.rs index 830d42efe..edf993b59 100644 --- a/src/models/graph/optimal_linear_arrangement.rs +++ b/src/models/graph/optimal_linear_arrangement.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -57,7 +57,7 @@ inventory::submit! { /// let problem = OptimalLinearArrangement::new(graph, 3); /// /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -145,7 +145,7 @@ where G: Graph + crate::variant::VariantParam, { const NAME: &'static str = "OptimalLinearArrangement"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -156,15 +156,13 @@ where vec![n; n] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl SatisfactionProblem for OptimalLinearArrangement {} - crate::declare_variants! { - default sat OptimalLinearArrangement => "2^num_vertices", + default OptimalLinearArrangement => "2^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/partial_feedback_edge_set.rs b/src/models/graph/partial_feedback_edge_set.rs index 03054578b..5806cd534 100644 --- a/src/models/graph/partial_feedback_edge_set.rs +++ b/src/models/graph/partial_feedback_edge_set.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; #[cfg(feature = "example-db")] use std::collections::BTreeSet; @@ -102,7 +102,7 @@ where G: Graph + crate::variant::VariantParam, { const NAME: &'static str = "PartialFeedbackEdgeSet"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -112,16 +112,11 @@ where vec![2; self.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl SatisfactionProblem for PartialFeedbackEdgeSet where - G: Graph + crate::variant::VariantParam -{ -} - fn has_cycle_with_length_at_most( graph: &G, kept_edges: &[bool], @@ -247,7 +242,7 @@ fn normalize_edge(u: usize, v: usize) -> (usize, usize) { } crate::declare_variants! { - default sat PartialFeedbackEdgeSet => "2^num_edges", + default PartialFeedbackEdgeSet => "2^num_edges", } #[cfg(test)] diff --git a/src/models/graph/partition_into_paths_of_length_2.rs b/src/models/graph/partition_into_paths_of_length_2.rs index b5ae20b51..a825e3d39 100644 --- a/src/models/graph/partition_into_paths_of_length_2.rs +++ b/src/models/graph/partition_into_paths_of_length_2.rs @@ -8,7 +8,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -54,7 +54,7 @@ inventory::submit! { /// let problem = PartitionIntoPathsOfLength2::new(graph); /// /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -148,7 +148,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "PartitionIntoPathsOfLength2"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -159,15 +159,13 @@ where vec![q; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_partition(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_partition(config)) } } -impl SatisfactionProblem for PartitionIntoPathsOfLength2 {} - crate::declare_variants! { - default sat PartitionIntoPathsOfLength2 => "3^num_vertices", + default PartitionIntoPathsOfLength2 => "3^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/partition_into_triangles.rs b/src/models/graph/partition_into_triangles.rs index 717757212..b14d5efe5 100644 --- a/src/models/graph/partition_into_triangles.rs +++ b/src/models/graph/partition_into_triangles.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -46,7 +46,7 @@ inventory::submit! { /// let problem = PartitionIntoTriangles::new(graph); /// /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -91,7 +91,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "PartitionIntoTriangles"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -102,62 +102,62 @@ where vec![q; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - let n = self.graph.num_vertices(); - let q = n / 3; - - // Check config length - if config.len() != n { - return false; - } - - // Check all values are in range [0, q) - if config.iter().any(|&c| c >= q) { - return false; - } - - // Count vertices per group - let mut counts = vec![0usize; q]; - for &c in config { - counts[c] += 1; - } - - // Each group must have exactly 3 vertices - if counts.iter().any(|&c| c != 3) { - return false; - } - - // Build per-group vertex lists in a single pass over config. - let mut group_verts = vec![[0usize; 3]; q]; - let mut group_pos = vec![0usize; q]; - - for (v, &g) in config.iter().enumerate() { - let pos = group_pos[g]; - group_verts[g][pos] = v; - group_pos[g] = pos + 1; - } - - // Check each group forms a triangle - for verts in &group_verts { - if !self.graph.has_edge(verts[0], verts[1]) { - return false; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n = self.graph.num_vertices(); + let q = n / 3; + + // Check config length + if config.len() != n { + return crate::types::Or(false); + } + + // Check all values are in range [0, q) + if config.iter().any(|&c| c >= q) { + return crate::types::Or(false); } - if !self.graph.has_edge(verts[0], verts[2]) { - return false; + + // Count vertices per group + let mut counts = vec![0usize; q]; + for &c in config { + counts[c] += 1; } - if !self.graph.has_edge(verts[1], verts[2]) { - return false; + + // Each group must have exactly 3 vertices + if counts.iter().any(|&c| c != 3) { + return crate::types::Or(false); } - } - true + // Build per-group vertex lists in a single pass over config. + let mut group_verts = vec![[0usize; 3]; q]; + let mut group_pos = vec![0usize; q]; + + for (v, &g) in config.iter().enumerate() { + let pos = group_pos[g]; + group_verts[g][pos] = v; + group_pos[g] = pos + 1; + } + + // Check each group forms a triangle + for verts in &group_verts { + if !self.graph.has_edge(verts[0], verts[1]) { + return crate::types::Or(false); + } + if !self.graph.has_edge(verts[0], verts[2]) { + return crate::types::Or(false); + } + if !self.graph.has_edge(verts[1], verts[2]) { + return crate::types::Or(false); + } + } + + true + }) } } -impl SatisfactionProblem for PartitionIntoTriangles {} - crate::declare_variants! { - default sat PartitionIntoTriangles => "2^num_vertices", + default PartitionIntoTriangles => "2^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/path_constrained_network_flow.rs b/src/models/graph/path_constrained_network_flow.rs index d034eae3e..373598e22 100644 --- a/src/models/graph/path_constrained_network_flow.rs +++ b/src/models/graph/path_constrained_network_flow.rs @@ -8,7 +8,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -216,7 +216,7 @@ impl PathConstrainedNetworkFlow { impl Problem for PathConstrainedNetworkFlow { const NAME: &'static str = "PathConstrainedNetworkFlow"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { self.paths @@ -225,8 +225,8 @@ impl Problem for PathConstrainedNetworkFlow { .collect() } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_feasible(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_feasible(config)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -234,10 +234,8 @@ impl Problem for PathConstrainedNetworkFlow { } } -impl SatisfactionProblem for PathConstrainedNetworkFlow {} - crate::declare_variants! { - default sat PathConstrainedNetworkFlow => "(max_capacity + 1)^num_paths", + default PathConstrainedNetworkFlow => "(max_capacity + 1)^num_paths", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/rooted_tree_arrangement.rs b/src/models/graph/rooted_tree_arrangement.rs index db6a721da..6fc20b366 100644 --- a/src/models/graph/rooted_tree_arrangement.rs +++ b/src/models/graph/rooted_tree_arrangement.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -100,7 +100,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "RootedTreeArrangement"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -111,13 +111,11 @@ where vec![n; 2 * n] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl SatisfactionProblem for RootedTreeArrangement {} - fn analyze_parent_array(parent: &[usize]) -> Option { let n = parent.len(); if n == 0 { @@ -207,7 +205,7 @@ fn are_ancestor_comparable(parent: &[usize], u: usize, v: usize) -> bool { } crate::declare_variants! { - default sat RootedTreeArrangement => "2^num_vertices", + default RootedTreeArrangement => "2^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/rural_postman.rs b/src/models/graph/rural_postman.rs index e77842b45..191c1a4d2 100644 --- a/src/models/graph/rural_postman.rs +++ b/src/models/graph/rural_postman.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -252,7 +252,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "RuralPostman"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -262,20 +262,13 @@ where vec![3; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl SatisfactionProblem for RuralPostman -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ -} - crate::declare_variants! { - default sat RuralPostman => "2^num_vertices * num_vertices^2", + default RuralPostman => "2^num_vertices * num_vertices^2", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/shortest_weight_constrained_path.rs b/src/models/graph/shortest_weight_constrained_path.rs index 2f79a9e41..faba36a79 100644 --- a/src/models/graph/shortest_weight_constrained_path.rs +++ b/src/models/graph/shortest_weight_constrained_path.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -308,7 +308,7 @@ where N: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "ShortestWeightConstrainedPath"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, N] @@ -318,18 +318,11 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl SatisfactionProblem for ShortestWeightConstrainedPath -where - G: Graph + crate::variant::VariantParam, - N: WeightElement + crate::variant::VariantParam, -{ -} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -361,7 +354,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "2^num_edges", + default ShortestWeightConstrainedPath => "2^num_edges", } #[cfg(test)] diff --git a/src/models/graph/spin_glass.rs b/src/models/graph/spin_glass.rs index 92ba26c67..0e86144a5 100644 --- a/src/models/graph/spin_glass.rs +++ b/src/models/graph/spin_glass.rs @@ -4,8 +4,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, WeightElement}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -56,7 +56,7 @@ inventory::submit! { /// let problem = SpinGlass::::new(2, vec![((0, 1), 1.0)], vec![0.0, 0.0]); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Ground state has opposite spins /// for sol in &solutions { @@ -220,15 +220,15 @@ where + From, { const NAME: &'static str = "SpinGlass"; - type Metric = SolutionSize; + type Value = Min; fn dims(&self) -> Vec { vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let spins = Self::config_to_spins(config); - SolutionSize::Valid(self.compute_energy(&spins).to_sum()) + Min(Some(self.compute_energy(&spins).to_sum())) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -236,29 +236,9 @@ where } } -impl OptimizationProblem for SpinGlass -where - G: Graph + crate::variant::VariantParam, - W: WeightElement - + crate::variant::VariantParam - + PartialOrd - + num_traits::Num - + num_traits::Zero - + num_traits::Bounded - + std::ops::AddAssign - + std::ops::Mul - + From, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Minimize - } -} - crate::declare_variants! { - default opt SpinGlass => "2^num_spins", - opt SpinGlass => "2^num_spins", + default SpinGlass => "2^num_spins", + SpinGlass => "2^num_spins", } #[cfg(feature = "example-db")] @@ -278,7 +258,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -231,9 +231,9 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !is_valid_steiner_tree(&self.graph, &self.terminals, config) { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (idx, &selected) in config.iter().enumerate() { @@ -243,25 +243,13 @@ where } } } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for SteinerTree -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Minimize + Min(Some(total)) } } crate::declare_variants! { - default opt SteinerTree => "3^num_terminals * num_vertices + 2^num_terminals * num_vertices^2", - opt SteinerTree => "3^num_terminals * num_vertices + 2^num_terminals * num_vertices^2", + default SteinerTree => "3^num_terminals * num_vertices + 2^num_terminals * num_vertices^2", + SteinerTree => "3^num_terminals * num_vertices + 2^num_terminals * num_vertices^2", } #[cfg(feature = "example-db")] @@ -277,7 +265,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -185,13 +185,13 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if config.len() != self.graph.num_edges() { - return SolutionSize::Invalid; + return Min(None); } let selected: Vec = config.iter().map(|&s| s == 1).collect(); if !is_steiner_tree(&self.graph, &self.terminals, &selected) { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (idx, &sel) in config.iter().enumerate() { @@ -201,19 +201,7 @@ where } } } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for SteinerTreeInGraphs -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Minimize + Min(Some(total)) } } @@ -286,8 +274,8 @@ pub(crate) fn is_steiner_tree(graph: &G, terminals: &[usize], selected } crate::declare_variants! { - default opt SteinerTreeInGraphs => "2^num_terminals * num_vertices^3", - opt SteinerTreeInGraphs => "2^num_terminals * num_vertices^3", + default SteinerTreeInGraphs => "2^num_terminals * num_vertices^3", + SteinerTreeInGraphs => "2^num_terminals * num_vertices^3", } #[cfg(feature = "example-db")] @@ -304,7 +292,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -189,18 +189,13 @@ where vec![2; self.candidate_arcs.len()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.evaluate_config(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.evaluate_config(config)) } } -impl SatisfactionProblem for StrongConnectivityAugmentation where - W: WeightElement + crate::variant::VariantParam -{ -} - crate::declare_variants! { - default sat StrongConnectivityAugmentation => "2^num_potential_arcs", + default StrongConnectivityAugmentation => "2^num_potential_arcs", } #[derive(Deserialize)] diff --git a/src/models/graph/subgraph_isomorphism.rs b/src/models/graph/subgraph_isomorphism.rs index 50f9654ec..ca7f7506b 100644 --- a/src/models/graph/subgraph_isomorphism.rs +++ b/src/models/graph/subgraph_isomorphism.rs @@ -7,7 +7,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -58,7 +58,7 @@ inventory::submit! { /// assert!(problem.evaluate(&[0, 1, 2])); /// /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -114,13 +114,13 @@ impl SubgraphIsomorphism { /// Check if a configuration represents a valid subgraph isomorphism. pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } } impl Problem for SubgraphIsomorphism { const NAME: &'static str = "SubgraphIsomorphism"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { let n_host = self.host_graph.num_vertices(); @@ -134,42 +134,44 @@ impl Problem for SubgraphIsomorphism { } } - fn evaluate(&self, config: &[usize]) -> bool { - let n_pattern = self.pattern_graph.num_vertices(); - let n_host = self.host_graph.num_vertices(); + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n_pattern = self.pattern_graph.num_vertices(); + let n_host = self.host_graph.num_vertices(); - // If the pattern has more vertices than the host, no injective mapping exists. - if n_pattern > n_host { - return false; - } + // If the pattern has more vertices than the host, no injective mapping exists. + if n_pattern > n_host { + return crate::types::Or(false); + } - // Config must have one entry per pattern vertex - if config.len() != n_pattern { - return false; - } + // Config must have one entry per pattern vertex + if config.len() != n_pattern { + return crate::types::Or(false); + } - // All values must be valid host vertex indices - if config.iter().any(|&v| v >= n_host) { - return false; - } + // All values must be valid host vertex indices + if config.iter().any(|&v| v >= n_host) { + return crate::types::Or(false); + } - // Check injectivity: all mapped host vertices must be distinct - for i in 0..n_pattern { - for j in (i + 1)..n_pattern { - if config[i] == config[j] { - return false; + // Check injectivity: all mapped host vertices must be distinct + for i in 0..n_pattern { + for j in (i + 1)..n_pattern { + if config[i] == config[j] { + return crate::types::Or(false); + } } } - } - // Check edge preservation: every pattern edge must map to a host edge - for (u, v) in self.pattern_graph.edges() { - if !self.host_graph.has_edge(config[u], config[v]) { - return false; + // Check edge preservation: every pattern edge must map to a host edge + for (u, v) in self.pattern_graph.edges() { + if !self.host_graph.has_edge(config[u], config[v]) { + return crate::types::Or(false); + } } - } - true + true + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -177,10 +179,8 @@ impl Problem for SubgraphIsomorphism { } } -impl SatisfactionProblem for SubgraphIsomorphism {} - crate::declare_variants! { - default sat SubgraphIsomorphism => "num_host_vertices ^ num_pattern_vertices", + default SubgraphIsomorphism => "num_host_vertices ^ num_pattern_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/traveling_salesman.rs b/src/models/graph/traveling_salesman.rs index 76cffc00b..017fabf7c 100644 --- a/src/models/graph/traveling_salesman.rs +++ b/src/models/graph/traveling_salesman.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -150,7 +150,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "TravelingSalesman"; - type Metric = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -160,9 +160,9 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !self.is_valid_hamiltonian_cycle(config) { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (idx, &selected) in config.iter().enumerate() { @@ -172,19 +172,7 @@ where } } } - SolutionSize::Valid(total) - } -} - -impl OptimizationProblem for TravelingSalesman -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Minimize + Min(Some(total)) } } @@ -267,12 +255,12 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "2^num_vertices", + default TravelingSalesman => "2^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/undirected_flow_lower_bounds.rs b/src/models/graph/undirected_flow_lower_bounds.rs index 0db1709d9..be86186dd 100644 --- a/src/models/graph/undirected_flow_lower_bounds.rs +++ b/src/models/graph/undirected_flow_lower_bounds.rs @@ -15,7 +15,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; @@ -137,7 +137,7 @@ impl UndirectedFlowLowerBounds { } pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } fn total_capacity(&self) -> Option { @@ -216,7 +216,7 @@ impl UndirectedFlowLowerBounds { impl Problem for UndirectedFlowLowerBounds { const NAME: &'static str = "UndirectedFlowLowerBounds"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -226,15 +226,13 @@ impl Problem for UndirectedFlowLowerBounds { vec![2; self.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.has_feasible_orientation(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.has_feasible_orientation(config)) } } -impl SatisfactionProblem for UndirectedFlowLowerBounds {} - crate::declare_variants! { - default sat UndirectedFlowLowerBounds => "2^num_edges", + default UndirectedFlowLowerBounds => "2^num_edges", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/undirected_two_commodity_integral_flow.rs b/src/models/graph/undirected_two_commodity_integral_flow.rs index 80019a5d1..15319893b 100644 --- a/src/models/graph/undirected_two_commodity_integral_flow.rs +++ b/src/models/graph/undirected_two_commodity_integral_flow.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -150,7 +150,7 @@ impl UndirectedTwoCommodityIntegralFlow { } pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } fn config_len(&self) -> usize { @@ -218,7 +218,7 @@ impl UndirectedTwoCommodityIntegralFlow { impl Problem for UndirectedTwoCommodityIntegralFlow { const NAME: &'static str = "UndirectedTwoCommodityIntegralFlow"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -234,66 +234,66 @@ impl Problem for UndirectedTwoCommodityIntegralFlow { .collect() } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.config_len() { - return false; - } - - for (edge_index, &capacity) in self.capacities.iter().enumerate() { - let Some(flows) = self.edge_flows(config, edge_index) else { - return false; - }; - - if flows - .iter() - .any(|&value| u64::try_from(value).map_or(true, |value| value > capacity)) - { - return false; - } - - if flows[0] > 0 && flows[1] > 0 { - return false; - } - if flows[2] > 0 && flows[3] > 0 { - return false; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.config_len() { + return crate::types::Or(false); } - let commodity_1 = u64::try_from(std::cmp::max(flows[0], flows[1])) - .expect("flow values already validated against u64 capacities"); - let commodity_2 = u64::try_from(std::cmp::max(flows[2], flows[3])) - .expect("flow values already validated against u64 capacities"); - let Some(shared) = commodity_1.checked_add(commodity_2) else { - return false; - }; - if shared > capacity { - return false; + for (edge_index, &capacity) in self.capacities.iter().enumerate() { + let Some(flows) = self.edge_flows(config, edge_index) else { + return crate::types::Or(false); + }; + + if flows + .iter() + .any(|&value| u64::try_from(value).map_or(true, |value| value > capacity)) + { + return crate::types::Or(false); + } + + if flows[0] > 0 && flows[1] > 0 { + return crate::types::Or(false); + } + if flows[2] > 0 && flows[3] > 0 { + return crate::types::Or(false); + } + + let commodity_1 = u64::try_from(std::cmp::max(flows[0], flows[1])) + .expect("flow values already validated against u64 capacities"); + let commodity_2 = u64::try_from(std::cmp::max(flows[2], flows[3])) + .expect("flow values already validated against u64 capacities"); + let Some(shared) = commodity_1.checked_add(commodity_2) else { + return crate::types::Or(false); + }; + if shared > capacity { + return crate::types::Or(false); + } } - } - for vertex in 0..self.num_vertices() { - if self.is_terminal(vertex) { - continue; - } + for vertex in 0..self.num_vertices() { + if self.is_terminal(vertex) { + continue; + } - if self.commodity_balance(config, 1, vertex) != Some(0) - || self.commodity_balance(config, 2, vertex) != Some(0) - { - return false; + if self.commodity_balance(config, 1, vertex) != Some(0) + || self.commodity_balance(config, 2, vertex) != Some(0) + { + return crate::types::Or(false); + } } - } - self.net_flow_into_sink(config, 1) - .is_some_and(|flow| flow >= self.requirement_1) - && self - .net_flow_into_sink(config, 2) - .is_some_and(|flow| flow >= self.requirement_2) + self.net_flow_into_sink(config, 1) + .is_some_and(|flow| flow >= self.requirement_1) + && self + .net_flow_into_sink(config, 2) + .is_some_and(|flow| flow >= self.requirement_2) + }) } } -impl SatisfactionProblem for UndirectedTwoCommodityIntegralFlow {} - crate::declare_variants! { - default sat UndirectedTwoCommodityIntegralFlow => "5^num_edges", + default UndirectedTwoCommodityIntegralFlow => "5^num_edges", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/additional_key.rs b/src/models/misc/additional_key.rs index 8f0a60ea3..6073fc46c 100644 --- a/src/models/misc/additional_key.rs +++ b/src/models/misc/additional_key.rs @@ -8,7 +8,7 @@ //! The problem is NP-complete (Garey & Johnson, SR7). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -56,7 +56,7 @@ inventory::submit! { /// vec![], /// ); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -192,7 +192,7 @@ impl AdditionalKey { impl Problem for AdditionalKey { const NAME: &'static str = "AdditionalKey"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -202,62 +202,62 @@ impl Problem for AdditionalKey { vec![2; self.relation_attrs.len()] } - fn evaluate(&self, config: &[usize]) -> bool { - // Check config length - if config.len() != self.relation_attrs.len() { - return false; - } - // Check all values are 0 or 1 - if config.iter().any(|&v| v >= 2) { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + // Check config length + if config.len() != self.relation_attrs.len() { + return crate::types::Or(false); + } + // Check all values are 0 or 1 + if config.iter().any(|&v| v >= 2) { + return crate::types::Or(false); + } - // Build selected attribute set - let selected: Vec = config - .iter() - .enumerate() - .filter(|(_, &v)| v == 1) - .map(|(i, _)| self.relation_attrs[i]) - .collect(); + // Build selected attribute set + let selected: Vec = config + .iter() + .enumerate() + .filter(|(_, &v)| v == 1) + .map(|(i, _)| self.relation_attrs[i]) + .collect(); - // Empty selection is not a key - if selected.is_empty() { - return false; - } + // Empty selection is not a key + if selected.is_empty() { + return crate::types::Or(false); + } - // Compute closure of selected attributes - let mut attr_set = vec![false; self.num_attributes]; - for &a in &selected { - attr_set[a] = true; - } - let closure = self.compute_closure(&attr_set); + // Compute closure of selected attributes + let mut attr_set = vec![false; self.num_attributes]; + for &a in &selected { + attr_set[a] = true; + } + let closure = self.compute_closure(&attr_set); - // Check closure covers all relation_attrs - if !self.relation_attrs.iter().all(|&a| closure[a]) { - return false; - } + // Check closure covers all relation_attrs + if !self.relation_attrs.iter().all(|&a| closure[a]) { + return crate::types::Or(false); + } - // Check minimality: removing any single selected attribute should break coverage - for &a in &selected { - let mut reduced = attr_set.clone(); - reduced[a] = false; - let reduced_closure = self.compute_closure(&reduced); - if self.relation_attrs.iter().all(|&ra| reduced_closure[ra]) { - return false; // Not minimal + // Check minimality: removing any single selected attribute should break coverage + for &a in &selected { + let mut reduced = attr_set.clone(); + reduced[a] = false; + let reduced_closure = self.compute_closure(&reduced); + if self.relation_attrs.iter().all(|&ra| reduced_closure[ra]) { + return crate::types::Or(false); // Not minimal + } } - } - // Build sorted selected vec and check it's not in known_keys - let mut sorted_selected = selected; - sorted_selected.sort_unstable(); - !self.known_keys.contains(&sorted_selected) + // Build sorted selected vec and check it's not in known_keys + let mut sorted_selected = selected; + sorted_selected.sort_unstable(); + !self.known_keys.contains(&sorted_selected) + }) } } -impl SatisfactionProblem for AdditionalKey {} - crate::declare_variants! { - default sat AdditionalKey => "2^num_relation_attrs * num_dependencies * num_attributes", + default AdditionalKey => "2^num_relation_attrs * num_dependencies * num_attributes", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/bin_packing.rs b/src/models/misc/bin_packing.rs index 9843518da..a778c2395 100644 --- a/src/models/misc/bin_packing.rs +++ b/src/models/misc/bin_packing.rs @@ -4,8 +4,8 @@ //! that minimizes the number of bins used while respecting capacity constraints. use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, WeightElement}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -48,7 +48,7 @@ inventory::submit! { /// // 4 items with sizes [3, 3, 2, 2], capacity 5 /// let problem = BinPacking::new(vec![3, 3, 2, 2], 5); /// let solver = BruteForce::new(); -/// let solution = solver.find_best(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -87,7 +87,7 @@ where W::Sum: PartialOrd, { const NAME: &'static str = "BinPacking"; - type Metric = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -98,24 +98,12 @@ where vec![n; n] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !is_valid_packing(&self.sizes, &self.capacity, config) { - return SolutionSize::Invalid; + return Min(None); } let num_bins = count_bins(config); - SolutionSize::Valid(num_bins as i32) - } -} - -impl OptimizationProblem for BinPacking -where - W: WeightElement + crate::variant::VariantParam, - W::Sum: PartialOrd, -{ - type Value = i32; - - fn direction(&self) -> Direction { - Direction::Minimize + Min(Some(num_bins as i32)) } } @@ -154,8 +142,8 @@ fn count_bins(config: &[usize]) -> usize { } crate::declare_variants! { - default opt BinPacking => "2^num_items", - opt BinPacking => "2^num_items", + default BinPacking => "2^num_items", + BinPacking => "2^num_items", } #[cfg(feature = "example-db")] @@ -165,7 +153,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec::new(vec![3, 3, 4], 7)), optimal_config: vec![0, 1, 0], - optimal_value: serde_json::json!({"Valid": 2}), + optimal_value: serde_json::json!(2), }] } diff --git a/src/models/misc/boyce_codd_normal_form_violation.rs b/src/models/misc/boyce_codd_normal_form_violation.rs index 2348e816a..3e465c5bf 100644 --- a/src/models/misc/boyce_codd_normal_form_violation.rs +++ b/src/models/misc/boyce_codd_normal_form_violation.rs @@ -6,7 +6,7 @@ //! some but not all attributes of `A' \ X` — i.e., a witness to a BCNF violation. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -176,36 +176,38 @@ impl BoyceCoddNormalFormViolation { impl Problem for BoyceCoddNormalFormViolation { const NAME: &'static str = "BoyceCoddNormalFormViolation"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.target_subset.len()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.target_subset.len() || config.iter().any(|&v| v > 1) { - return false; - } - let x: HashSet = config - .iter() - .enumerate() - .filter(|(_, &v)| v == 1) - .map(|(i, _)| self.target_subset[i]) - .collect(); - let closure = Self::compute_closure(&x, &self.functional_deps); - // Check: ∃ y, z ∈ A' \ X s.t. y ∈ closure ∧ z ∉ closure - let mut has_in_closure = false; - let mut has_not_in_closure = false; - for &a in &self.target_subset { - if !x.contains(&a) { - if closure.contains(&a) { - has_in_closure = true; - } else { - has_not_in_closure = true; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.target_subset.len() || config.iter().any(|&v| v > 1) { + return crate::types::Or(false); + } + let x: HashSet = config + .iter() + .enumerate() + .filter(|(_, &v)| v == 1) + .map(|(i, _)| self.target_subset[i]) + .collect(); + let closure = Self::compute_closure(&x, &self.functional_deps); + // Check: ∃ y, z ∈ A' \ X s.t. y ∈ closure ∧ z ∉ closure + let mut has_in_closure = false; + let mut has_not_in_closure = false; + for &a in &self.target_subset { + if !x.contains(&a) { + if closure.contains(&a) { + has_in_closure = true; + } else { + has_not_in_closure = true; + } } } - } - has_in_closure && has_not_in_closure + has_in_closure && has_not_in_closure + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -213,10 +215,8 @@ impl Problem for BoyceCoddNormalFormViolation { } } -impl SatisfactionProblem for BoyceCoddNormalFormViolation {} - crate::declare_variants! { - default sat BoyceCoddNormalFormViolation => "2^num_target_attributes * num_target_attributes^2 * num_functional_deps", + default BoyceCoddNormalFormViolation => "2^num_target_attributes * num_target_attributes^2 * num_functional_deps", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/capacity_assignment.rs b/src/models/misc/capacity_assignment.rs index 46d889b4c..a648f4b92 100644 --- a/src/models/misc/capacity_assignment.rs +++ b/src/models/misc/capacity_assignment.rs @@ -5,7 +5,7 @@ //! their respective budgets. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -155,17 +155,19 @@ impl CapacityAssignment { impl Problem for CapacityAssignment { const NAME: &'static str = "CapacityAssignment"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.num_capacities(); self.num_links()] } - fn evaluate(&self, config: &[usize]) -> bool { - let Some((total_cost, total_delay)) = self.total_cost_and_delay(config) else { - return false; - }; - total_cost <= self.cost_budget as u128 && total_delay <= self.delay_budget as u128 + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let Some((total_cost, total_delay)) = self.total_cost_and_delay(config) else { + return crate::types::Or(false); + }; + total_cost <= self.cost_budget as u128 && total_delay <= self.delay_budget as u128 + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -173,10 +175,8 @@ impl Problem for CapacityAssignment { } } -impl SatisfactionProblem for CapacityAssignment {} - crate::declare_variants! { - default sat CapacityAssignment => "num_capacities ^ num_links", + default CapacityAssignment => "num_capacities ^ num_links", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/conjunctive_boolean_query.rs b/src/models/misc/conjunctive_boolean_query.rs index 9dd0631f0..e9e4b8737 100644 --- a/src/models/misc/conjunctive_boolean_query.rs +++ b/src/models/misc/conjunctive_boolean_query.rs @@ -11,7 +11,7 @@ //! variables such that every conjunct's resolved tuple belongs to its relation. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -76,7 +76,7 @@ pub enum QueryArg { /// ]; /// let problem = ConjunctiveBooleanQuery::new(6, relations, 1, conjuncts); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -191,7 +191,7 @@ impl ConjunctiveBooleanQuery { impl Problem for ConjunctiveBooleanQuery { const NAME: &'static str = "ConjunctiveBooleanQuery"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -201,30 +201,30 @@ impl Problem for ConjunctiveBooleanQuery { vec![self.domain_size; self.num_variables] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_variables { - return false; - } - if config.iter().any(|&v| v >= self.domain_size) { - return false; - } - self.conjuncts.iter().all(|(rel_idx, args)| { - let tuple: Vec = args - .iter() - .map(|arg| match arg { - QueryArg::Variable(i) => config[*i], - QueryArg::Constant(c) => *c, - }) - .collect(); - self.relations[*rel_idx].tuples.contains(&tuple) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_variables { + return crate::types::Or(false); + } + if config.iter().any(|&v| v >= self.domain_size) { + return crate::types::Or(false); + } + self.conjuncts.iter().all(|(rel_idx, args)| { + let tuple: Vec = args + .iter() + .map(|arg| match arg { + QueryArg::Variable(i) => config[*i], + QueryArg::Constant(c) => *c, + }) + .collect(); + self.relations[*rel_idx].tuples.contains(&tuple) + }) }) } } -impl SatisfactionProblem for ConjunctiveBooleanQuery {} - crate::declare_variants! { - default sat ConjunctiveBooleanQuery => "domain_size ^ num_variables", + default ConjunctiveBooleanQuery => "domain_size ^ num_variables", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/conjunctive_query_foldability.rs b/src/models/misc/conjunctive_query_foldability.rs index 87b08299e..cd3963c50 100644 --- a/src/models/misc/conjunctive_query_foldability.rs +++ b/src/models/misc/conjunctive_query_foldability.rs @@ -5,7 +5,7 @@ //! that transforms Q1 into Q2. NP-complete (Chandra & Merlin, 1977). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -81,7 +81,7 @@ pub enum Term { /// ], /// ); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -262,7 +262,7 @@ impl ConjunctiveQueryFoldability { impl Problem for ConjunctiveQueryFoldability { const NAME: &'static str = "ConjunctiveQueryFoldability"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -284,39 +284,40 @@ impl Problem for ConjunctiveQueryFoldability { /// /// Returns `true` iff applying the substitution encoded by `config` to every /// atom of Q1 produces exactly the set of atoms in Q2. - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_undistinguished { - return false; - } - let range = self.domain_size + self.num_distinguished + self.num_undistinguished; - if config.iter().any(|&v| v >= range) { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_undistinguished { + return crate::types::Or(false); + } + let range = self.domain_size + self.num_distinguished + self.num_undistinguished; + if config.iter().any(|&v| v >= range) { + return crate::types::Or(false); + } - // Apply σ to every atom of Q1. - let substituted: HashSet<(usize, Vec)> = self - .query1_conjuncts - .iter() - .map(|(rel_idx, args)| { - let new_args = args - .iter() - .map(|term| self.apply_substitution(term, config)) - .collect(); - (*rel_idx, new_args) - }) - .collect(); + // Apply σ to every atom of Q1. + let substituted: HashSet<(usize, Vec)> = self + .query1_conjuncts + .iter() + .map(|(rel_idx, args)| { + let new_args = args + .iter() + .map(|term| self.apply_substitution(term, config)) + .collect(); + (*rel_idx, new_args) + }) + .collect(); - // Collect Q2 as a set. - let q2_set: HashSet<(usize, Vec)> = self.query2_conjuncts.iter().cloned().collect(); + // Collect Q2 as a set. + let q2_set: HashSet<(usize, Vec)> = + self.query2_conjuncts.iter().cloned().collect(); - substituted == q2_set + substituted == q2_set + }) } } -impl SatisfactionProblem for ConjunctiveQueryFoldability {} - crate::declare_variants! { - default sat ConjunctiveQueryFoldability => "(num_distinguished + num_undistinguished + domain_size)^num_undistinguished * num_conjuncts_q1", + default ConjunctiveQueryFoldability => "(num_distinguished + num_undistinguished + domain_size)^num_undistinguished * num_conjuncts_q1", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/consistency_of_database_frequency_tables.rs b/src/models/misc/consistency_of_database_frequency_tables.rs index f052d42e3..04ade7a9e 100644 --- a/src/models/misc/consistency_of_database_frequency_tables.rs +++ b/src/models/misc/consistency_of_database_frequency_tables.rs @@ -7,7 +7,7 @@ //! frequency table and every known value. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; @@ -278,7 +278,7 @@ impl ConsistencyOfDatabaseFrequencyTables { impl Problem for ConsistencyOfDatabaseFrequencyTables { const NAME: &'static str = "ConsistencyOfDatabaseFrequencyTables"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -292,51 +292,51 @@ impl Problem for ConsistencyOfDatabaseFrequencyTables { dims } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_assignment_variables() { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_assignment_variables() { + return crate::types::Or(false); + } - for object in 0..self.num_objects { - for (attribute, &domain_size) in self.attribute_domains.iter().enumerate() { - if config[self.config_index(object, attribute)] >= domain_size { - return false; + for object in 0..self.num_objects { + for (attribute, &domain_size) in self.attribute_domains.iter().enumerate() { + if config[self.config_index(object, attribute)] >= domain_size { + return crate::types::Or(false); + } } } - } - for known_value in &self.known_values { - if config[self.config_index(known_value.object(), known_value.attribute())] - != known_value.value() - { - return false; + for known_value in &self.known_values { + if config[self.config_index(known_value.object(), known_value.attribute())] + != known_value.value() + { + return crate::types::Or(false); + } } - } - for table in &self.frequency_tables { - let rows = self.attribute_domains[table.attribute_a()]; - let cols = self.attribute_domains[table.attribute_b()]; - let mut observed = vec![vec![0usize; cols]; rows]; + for table in &self.frequency_tables { + let rows = self.attribute_domains[table.attribute_a()]; + let cols = self.attribute_domains[table.attribute_b()]; + let mut observed = vec![vec![0usize; cols]; rows]; - for object in 0..self.num_objects { - let value_a = config[self.config_index(object, table.attribute_a())]; - let value_b = config[self.config_index(object, table.attribute_b())]; - observed[value_a][value_b] += 1; - } + for object in 0..self.num_objects { + let value_a = config[self.config_index(object, table.attribute_a())]; + let value_b = config[self.config_index(object, table.attribute_b())]; + observed[value_a][value_b] += 1; + } - if observed != table.counts { - return false; + if observed != table.counts { + return crate::types::Or(false); + } } - } - true + true + }) } } -impl SatisfactionProblem for ConsistencyOfDatabaseFrequencyTables {} - crate::declare_variants! { - default sat ConsistencyOfDatabaseFrequencyTables => "domain_size_product^num_objects", + default ConsistencyOfDatabaseFrequencyTables => "domain_size_product^num_objects", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/ensemble_computation.rs b/src/models/misc/ensemble_computation.rs index 0f710a072..0de0d77f0 100644 --- a/src/models/misc/ensemble_computation.rs +++ b/src/models/misc/ensemble_computation.rs @@ -1,7 +1,7 @@ //! Ensemble Computation problem implementation. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -146,47 +146,49 @@ impl EnsembleComputation { impl Problem for EnsembleComputation { const NAME: &'static str = "EnsembleComputation"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.universe_size + self.budget; 2 * self.budget] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != 2 * self.budget { - return false; - } - - let Some(required_subsets) = self.required_subsets() else { - return false; - }; - if required_subsets.is_empty() { - return true; - } - - let mut computed = Vec::with_capacity(self.budget); - for step in 0..self.budget { - let left_operand = config[2 * step]; - let right_operand = config[2 * step + 1]; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != 2 * self.budget { + return crate::types::Or(false); + } - let Some(left) = self.decode_operand(left_operand, &computed) else { - return false; + let Some(required_subsets) = self.required_subsets() else { + return crate::types::Or(false); }; - let Some(right) = self.decode_operand(right_operand, &computed) else { - return false; - }; - - if !Self::are_disjoint(&left, &right) { - return false; + if required_subsets.is_empty() { + return crate::types::Or(true); } - computed.push(Self::union_disjoint(&left, &right)); - if Self::all_required_subsets_present(&required_subsets, &computed) { - return true; + let mut computed = Vec::with_capacity(self.budget); + for step in 0..self.budget { + let left_operand = config[2 * step]; + let right_operand = config[2 * step + 1]; + + let Some(left) = self.decode_operand(left_operand, &computed) else { + return crate::types::Or(false); + }; + let Some(right) = self.decode_operand(right_operand, &computed) else { + return crate::types::Or(false); + }; + + if !Self::are_disjoint(&left, &right) { + return crate::types::Or(false); + } + + computed.push(Self::union_disjoint(&left, &right)); + if Self::all_required_subsets_present(&required_subsets, &computed) { + return crate::types::Or(true); + } } - } - false + false + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -194,10 +196,8 @@ impl Problem for EnsembleComputation { } } -impl SatisfactionProblem for EnsembleComputation {} - crate::declare_variants! { - default sat EnsembleComputation => "(universe_size + budget)^(2 * budget)", + default EnsembleComputation => "(universe_size + budget)^(2 * budget)", } #[derive(Debug, Clone, Deserialize)] diff --git a/src/models/misc/expected_retrieval_cost.rs b/src/models/misc/expected_retrieval_cost.rs index 640885691..38725c85b 100644 --- a/src/models/misc/expected_retrieval_cost.rs +++ b/src/models/misc/expected_retrieval_cost.rs @@ -5,7 +5,7 @@ //! prescribed bound. use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; const FLOAT_TOLERANCE: f64 = 1e-9; @@ -126,7 +126,7 @@ impl ExpectedRetrievalCost { impl Problem for ExpectedRetrievalCost { const NAME: &'static str = "ExpectedRetrievalCost"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -136,13 +136,11 @@ impl Problem for ExpectedRetrievalCost { vec![self.num_sectors; self.num_records()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl SatisfactionProblem for ExpectedRetrievalCost {} - fn latency_distance(num_sectors: usize, source: usize, target: usize) -> usize { if source < target { target - source - 1 @@ -152,7 +150,7 @@ fn latency_distance(num_sectors: usize, source: usize, target: usize) -> usize { } crate::declare_variants! { - default sat ExpectedRetrievalCost => "num_sectors ^ num_records", + default ExpectedRetrievalCost => "num_sectors ^ num_records", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/factoring.rs b/src/models/misc/factoring.rs index a0ebc4cb7..9b72b2754 100644 --- a/src/models/misc/factoring.rs +++ b/src/models/misc/factoring.rs @@ -4,8 +4,8 @@ //! Given a number N, find two factors (a, b) such that a * b = N. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -39,7 +39,7 @@ inventory::submit! { /// let problem = Factoring::new(2, 2, 6); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Should find: 2*3=6 or 3*2=6 /// for sol in &solutions { @@ -134,13 +134,13 @@ pub(crate) fn is_factoring(target: u64, a: u64, b: u64) -> bool { impl Problem for Factoring { const NAME: &'static str = "Factoring"; - type Metric = SolutionSize; + type Value = Min; fn dims(&self) -> Vec { vec![2; self.m + self.n] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let (a, b) = self.read_factors(config); let product = a * b; // Distance from target (0 means exact match) @@ -149,7 +149,7 @@ impl Problem for Factoring { } else { (self.target - product) as i32 }; - SolutionSize::Valid(distance) + Min(Some(distance)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -157,16 +157,8 @@ impl Problem for Factoring { } } -impl OptimizationProblem for Factoring { - type Value = i32; - - fn direction(&self) -> Direction { - Direction::Minimize - } -} - crate::declare_variants! { - default opt Factoring => "exp((m + n)^(1/3) * log(m + n)^(2/3))", + default Factoring => "exp((m + n)^(1/3) * log(m + n)^(2/3))", } #[cfg(feature = "example-db")] @@ -175,7 +167,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -171,32 +171,32 @@ impl Problem for FlowShopScheduling { (0..n).rev().map(|i| i + 1).collect() } - fn evaluate(&self, config: &[usize]) -> bool { - let n = self.num_jobs(); - if config.len() != n { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n = self.num_jobs(); + if config.len() != n { + return crate::types::Or(false); + } - // Decode Lehmer code into a permutation. - // config[i] must be < n - i (the domain size for position i). - let mut available: Vec = (0..n).collect(); - let mut job_order = Vec::with_capacity(n); - for &c in config.iter() { - if c >= available.len() { - return false; + // Decode Lehmer code into a permutation. + // config[i] must be < n - i (the domain size for position i). + let mut available: Vec = (0..n).collect(); + let mut job_order = Vec::with_capacity(n); + for &c in config.iter() { + if c >= available.len() { + return crate::types::Or(false); + } + job_order.push(available.remove(c)); } - job_order.push(available.remove(c)); - } - let makespan = self.compute_makespan(&job_order); - makespan <= self.deadline + let makespan = self.compute_makespan(&job_order); + makespan <= self.deadline + }) } } -impl SatisfactionProblem for FlowShopScheduling {} - crate::declare_variants! { - default sat FlowShopScheduling => "factorial(num_jobs)", + default FlowShopScheduling => "factorial(num_jobs)", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/grouping_by_swapping.rs b/src/models/misc/grouping_by_swapping.rs index 34f9781be..ff6cc0d36 100644 --- a/src/models/misc/grouping_by_swapping.rs +++ b/src/models/misc/grouping_by_swapping.rs @@ -5,7 +5,7 @@ //! symbol appears in a single contiguous block. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -141,15 +141,17 @@ impl GroupingBySwapping { impl Problem for GroupingBySwapping { const NAME: &'static str = "GroupingBySwapping"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.string_len(); self.budget] } - fn evaluate(&self, config: &[usize]) -> bool { - self.apply_swap_program(config) - .is_some_and(|candidate| self.is_grouped(&candidate)) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + self.apply_swap_program(config) + .is_some_and(|candidate| self.is_grouped(&candidate)) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -157,10 +159,8 @@ impl Problem for GroupingBySwapping { } } -impl SatisfactionProblem for GroupingBySwapping {} - crate::declare_variants! { - default sat GroupingBySwapping => "string_len ^ budget", + default GroupingBySwapping => "string_len ^ budget", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/knapsack.rs b/src/models/misc/knapsack.rs index 65973fe8e..2c282fc8c 100644 --- a/src/models/misc/knapsack.rs +++ b/src/models/misc/knapsack.rs @@ -4,8 +4,8 @@ //! total value while respecting a weight capacity constraint. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Max; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -43,7 +43,7 @@ inventory::submit! { /// /// let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); /// let solver = BruteForce::new(); -/// let solution = solver.find_best(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -119,7 +119,7 @@ impl Knapsack { impl Problem for Knapsack { const NAME: &'static str = "Knapsack"; - type Metric = SolutionSize; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -129,12 +129,12 @@ impl Problem for Knapsack { vec![2; self.num_items()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if config.len() != self.num_items() { - return SolutionSize::Invalid; + return Max(None); } if config.iter().any(|&v| v >= 2) { - return SolutionSize::Invalid; + return Max(None); } let total_weight: i64 = config .iter() @@ -143,7 +143,7 @@ impl Problem for Knapsack { .map(|(i, _)| self.weights[i]) .sum(); if total_weight > self.capacity { - return SolutionSize::Invalid; + return Max(None); } let total_value: i64 = config .iter() @@ -151,20 +151,12 @@ impl Problem for Knapsack { .filter(|(_, &x)| x == 1) .map(|(i, _)| self.values[i]) .sum(); - SolutionSize::Valid(total_value) - } -} - -impl OptimizationProblem for Knapsack { - type Value = i64; - - fn direction(&self) -> Direction { - Direction::Maximize + Max(Some(total_value)) } } crate::declare_variants! { - default opt Knapsack => "2^(num_items / 2)", + default Knapsack => "2^(num_items / 2)", } mod nonnegative_i64 { @@ -211,7 +203,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec bool { impl Problem for LongestCommonSubsequence { const NAME: &'static str = "LongestCommonSubsequence"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -144,21 +144,21 @@ impl Problem for LongestCommonSubsequence { vec![self.alphabet_size; self.bound] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.bound { - return false; - } - if config.iter().any(|&symbol| symbol >= self.alphabet_size) { - return false; - } - self.strings.iter().all(|s| is_subsequence(config, s)) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.bound { + return crate::types::Or(false); + } + if config.iter().any(|&symbol| symbol >= self.alphabet_size) { + return crate::types::Or(false); + } + self.strings.iter().all(|s| is_subsequence(config, s)) + }) } } -impl SatisfactionProblem for LongestCommonSubsequence {} - crate::declare_variants! { - default sat LongestCommonSubsequence => "alphabet_size ^ bound", + default LongestCommonSubsequence => "alphabet_size ^ bound", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/minimum_tardiness_sequencing.rs b/src/models/misc/minimum_tardiness_sequencing.rs index ad49f5eb0..17a7fe3d9 100644 --- a/src/models/misc/minimum_tardiness_sequencing.rs +++ b/src/models/misc/minimum_tardiness_sequencing.rs @@ -6,8 +6,8 @@ //! Corresponds to scheduling notation `1|prec, pj=1|sum Uj`. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -52,7 +52,7 @@ inventory::submit! { /// vec![(0, 2)], // task 0 must precede task 2 /// ); /// let solver = BruteForce::new(); -/// let solution = solver.find_best(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -125,7 +125,7 @@ impl MinimumTardinessSequencing { impl Problem for MinimumTardinessSequencing { const NAME: &'static str = "MinimumTardinessSequencing"; - type Metric = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -136,10 +136,10 @@ impl Problem for MinimumTardinessSequencing { (0..n).rev().map(|i| i + 1).collect() } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let n = self.num_tasks; if config.len() != n { - return SolutionSize::Invalid; + return Min(None); } // Decode Lehmer code into a permutation. @@ -148,7 +148,7 @@ impl Problem for MinimumTardinessSequencing { let mut schedule = Vec::with_capacity(n); for &c in config.iter() { if c >= available.len() { - return SolutionSize::Invalid; + return Min(None); } schedule.push(available.remove(c)); } @@ -163,7 +163,7 @@ impl Problem for MinimumTardinessSequencing { // Check precedence constraints: for each (pred, succ), sigma(pred) < sigma(succ) for &(pred, succ) in &self.precedences { if sigma[pred] >= sigma[succ] { - return SolutionSize::Invalid; + return Min(None); } } @@ -174,20 +174,12 @@ impl Problem for MinimumTardinessSequencing { .filter(|&(t, &pos)| pos + 1 > self.deadlines[t]) .count(); - SolutionSize::Valid(tardy_count) - } -} - -impl OptimizationProblem for MinimumTardinessSequencing { - type Value = usize; - - fn direction(&self) -> Direction { - Direction::Minimize + Min(Some(tardy_count)) } } crate::declare_variants! { - default opt MinimumTardinessSequencing => "2^num_tasks", + default MinimumTardinessSequencing => "2^num_tasks", } #[cfg(feature = "example-db")] @@ -203,7 +195,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -115,26 +115,26 @@ impl Problem for MultiprocessorScheduling { vec![self.num_processors; self.num_tasks()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_tasks() { - return false; - } - let m = self.num_processors; - if config.iter().any(|&p| p >= m) { - return false; - } - let mut loads = vec![0u64; m]; - for (i, &processor) in config.iter().enumerate() { - loads[processor] += self.lengths[i]; - } - loads.iter().all(|&load| load <= self.deadline) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_tasks() { + return crate::types::Or(false); + } + let m = self.num_processors; + if config.iter().any(|&p| p >= m) { + return crate::types::Or(false); + } + let mut loads = vec![0u64; m]; + for (i, &processor) in config.iter().enumerate() { + loads[processor] += self.lengths[i]; + } + loads.iter().all(|&load| load <= self.deadline) + }) } } -impl SatisfactionProblem for MultiprocessorScheduling {} - crate::declare_variants! { - default sat MultiprocessorScheduling => "2^num_tasks", + default MultiprocessorScheduling => "2^num_tasks", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/paintshop.rs b/src/models/misc/paintshop.rs index acce73f7e..ade889b1a 100644 --- a/src/models/misc/paintshop.rs +++ b/src/models/misc/paintshop.rs @@ -6,8 +6,8 @@ //! The goal is to minimize color switches between adjacent positions. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; @@ -40,7 +40,7 @@ inventory::submit! { /// let problem = PaintShop::new(vec!["a", "b", "a", "c", "c", "b"]); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // The minimum number of color switches /// for sol in &solutions { @@ -168,15 +168,15 @@ pub(crate) fn count_paint_switches(coloring: &[usize]) -> usize { impl Problem for PaintShop { const NAME: &'static str = "PaintShop"; - type Metric = SolutionSize; + type Value = Min; fn dims(&self) -> Vec { vec![2; self.num_cars] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { // All configurations are valid (no hard constraints). - SolutionSize::Valid(self.count_switches(config) as i32) + Min(Some(self.count_switches(config) as i32)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -184,16 +184,8 @@ impl Problem for PaintShop { } } -impl OptimizationProblem for PaintShop { - type Value = i32; - - fn direction(&self) -> Direction { - Direction::Minimize - } -} - crate::declare_variants! { - default opt PaintShop => "2^num_cars", + default PaintShop => "2^num_cars", } #[cfg(feature = "example-db")] @@ -202,7 +194,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -233,16 +233,16 @@ impl Problem for PartiallyOrderedKnapsack { vec![2; self.num_items()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if config.len() != self.num_items() { - return SolutionSize::Invalid; + return Max(None); } if config.iter().any(|&v| v >= 2) { - return SolutionSize::Invalid; + return Max(None); } // Check downward-closure (precedence constraints) if !self.is_downward_closed(config) { - return SolutionSize::Invalid; + return Max(None); } // Check capacity constraint let total_weight: i64 = config @@ -252,7 +252,7 @@ impl Problem for PartiallyOrderedKnapsack { .map(|(i, _)| self.weights[i]) .sum(); if total_weight > self.capacity { - return SolutionSize::Invalid; + return Max(None); } // Compute total value let total_value: i64 = config @@ -261,20 +261,12 @@ impl Problem for PartiallyOrderedKnapsack { .filter(|(_, &x)| x == 1) .map(|(i, _)| self.values[i]) .sum(); - SolutionSize::Valid(total_value) - } -} - -impl OptimizationProblem for PartiallyOrderedKnapsack { - type Value = i64; - - fn direction(&self) -> Direction { - Direction::Maximize + Max(Some(total_value)) } } crate::declare_variants! { - default opt PartiallyOrderedKnapsack => "2^num_items", + default PartiallyOrderedKnapsack => "2^num_items", } #[cfg(feature = "example-db")] @@ -288,7 +280,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -92,27 +92,27 @@ impl Problem for Partition { vec![2; self.num_elements()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_elements() { - return false; - } - if config.iter().any(|&v| v >= 2) { - return false; - } - let selected_sum: u64 = config - .iter() - .enumerate() - .filter(|(_, &x)| x == 1) - .map(|(i, _)| self.sizes[i]) - .sum(); - selected_sum * 2 == self.total_sum() + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_elements() { + return crate::types::Or(false); + } + if config.iter().any(|&v| v >= 2) { + return crate::types::Or(false); + } + let selected_sum: u64 = config + .iter() + .enumerate() + .filter(|(_, &x)| x == 1) + .map(|(i, _)| self.sizes[i]) + .sum(); + selected_sum * 2 == self.total_sum() + }) } } -impl SatisfactionProblem for Partition {} - crate::declare_variants! { - default sat Partition => "2^(num_elements / 2)", + default Partition => "2^(num_elements / 2)", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/precedence_constrained_scheduling.rs b/src/models/misc/precedence_constrained_scheduling.rs index 250458ce4..e5c887e07 100644 --- a/src/models/misc/precedence_constrained_scheduling.rs +++ b/src/models/misc/precedence_constrained_scheduling.rs @@ -5,7 +5,7 @@ //! respecting precedences. NP-complete via reduction from 3SAT (Ullman, 1975). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -47,7 +47,7 @@ inventory::submit! { /// // 4 tasks, 2 processors, deadline 3, with t0 < t2 and t1 < t3 /// let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -118,7 +118,7 @@ impl PrecedenceConstrainedScheduling { impl Problem for PrecedenceConstrainedScheduling { const NAME: &'static str = "PrecedenceConstrainedScheduling"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -128,36 +128,36 @@ impl Problem for PrecedenceConstrainedScheduling { vec![self.deadline; self.num_tasks] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_tasks { - return false; - } - // Check all values are valid time slots - if config.iter().any(|&v| v >= self.deadline) { - return false; - } - // Check processor capacity: at most num_processors tasks per time slot - let mut slot_count = vec![0usize; self.deadline]; - for &slot in config { - slot_count[slot] += 1; - if slot_count[slot] > self.num_processors { - return false; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_tasks { + return crate::types::Or(false); } - } - // Check precedence constraints: for (i, j), slot[j] >= slot[i] + 1 - for &(i, j) in &self.precedences { - if config[j] < config[i] + 1 { - return false; + // Check all values are valid time slots + if config.iter().any(|&v| v >= self.deadline) { + return crate::types::Or(false); } - } - true + // Check processor capacity: at most num_processors tasks per time slot + let mut slot_count = vec![0usize; self.deadline]; + for &slot in config { + slot_count[slot] += 1; + if slot_count[slot] > self.num_processors { + return crate::types::Or(false); + } + } + // Check precedence constraints: for (i, j), slot[j] >= slot[i] + 1 + for &(i, j) in &self.precedences { + if config[j] < config[i] + 1 { + return crate::types::Or(false); + } + } + true + }) } } -impl SatisfactionProblem for PrecedenceConstrainedScheduling {} - crate::declare_variants! { - default sat PrecedenceConstrainedScheduling => "2^num_tasks", + default PrecedenceConstrainedScheduling => "2^num_tasks", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/rectilinear_picture_compression.rs b/src/models/misc/rectilinear_picture_compression.rs index 01cb2c67c..13243e40d 100644 --- a/src/models/misc/rectilinear_picture_compression.rs +++ b/src/models/misc/rectilinear_picture_compression.rs @@ -7,7 +7,7 @@ //! and every covered entry must be 1. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::de::Deserializer; use serde::{Deserialize, Serialize}; @@ -55,7 +55,7 @@ inventory::submit! { /// ]; /// let problem = RectilinearPictureCompression::new(matrix, 2); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize)] @@ -234,7 +234,7 @@ impl RectilinearPictureCompression { impl Problem for RectilinearPictureCompression { const NAME: &'static str = "RectilinearPictureCompression"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -244,52 +244,52 @@ impl Problem for RectilinearPictureCompression { vec![2; self.maximal_rects.len()] } - fn evaluate(&self, config: &[usize]) -> bool { - let rects = &self.maximal_rects; - if config.len() != rects.len() { - return false; - } - if config.iter().any(|&v| v >= 2) { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let rects = &self.maximal_rects; + if config.len() != rects.len() { + return crate::types::Or(false); + } + if config.iter().any(|&v| v >= 2) { + return crate::types::Or(false); + } - // Count selected rectangles. - let selected_count: usize = config.iter().sum(); - if (selected_count as i64) > self.bound { - return false; - } + // Count selected rectangles. + let selected_count: usize = config.iter().sum(); + if (selected_count as i64) > self.bound { + return crate::types::Or(false); + } - // Check that all 1-entries are covered. - let m = self.num_rows(); - let n = self.num_cols(); - let mut covered = vec![vec![false; n]; m]; - for (i, &x) in config.iter().enumerate() { - if x == 1 { - let (r1, c1, r2, c2) = rects[i]; - for row in &mut covered[r1..=r2] { - for cell in &mut row[c1..=c2] { - *cell = true; + // Check that all 1-entries are covered. + let m = self.num_rows(); + let n = self.num_cols(); + let mut covered = vec![vec![false; n]; m]; + for (i, &x) in config.iter().enumerate() { + if x == 1 { + let (r1, c1, r2, c2) = rects[i]; + for row in &mut covered[r1..=r2] { + for cell in &mut row[c1..=c2] { + *cell = true; + } } } } - } - for (row_m, row_c) in self.matrix.iter().zip(covered.iter()) { - for (&entry, &cov) in row_m.iter().zip(row_c.iter()) { - if entry && !cov { - return false; + for (row_m, row_c) in self.matrix.iter().zip(covered.iter()) { + for (&entry, &cov) in row_m.iter().zip(row_c.iter()) { + if entry && !cov { + return crate::types::Or(false); + } } } - } - true + true + }) } } -impl SatisfactionProblem for RectilinearPictureCompression {} - crate::declare_variants! { - default sat RectilinearPictureCompression => "2^(num_rows * num_cols)", + default RectilinearPictureCompression => "2^(num_rows * num_cols)", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/resource_constrained_scheduling.rs b/src/models/misc/resource_constrained_scheduling.rs index 10de92907..4a714371f 100644 --- a/src/models/misc/resource_constrained_scheduling.rs +++ b/src/models/misc/resource_constrained_scheduling.rs @@ -5,7 +5,7 @@ //! processor capacity limit and resource usage constraints per time slot. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -53,7 +53,7 @@ inventory::submit! { /// 2, /// ); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -133,7 +133,7 @@ impl ResourceConstrainedScheduling { impl Problem for ResourceConstrainedScheduling { const NAME: &'static str = "ResourceConstrainedScheduling"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -143,61 +143,61 @@ impl Problem for ResourceConstrainedScheduling { vec![self.deadline as usize; self.num_tasks()] } - fn evaluate(&self, config: &[usize]) -> bool { - let n = self.num_tasks(); - let d = self.deadline as usize; - let r = self.num_resources(); + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n = self.num_tasks(); + let d = self.deadline as usize; + let r = self.num_resources(); - // Check config length - if config.len() != n { - return false; - } + // Check config length + if config.len() != n { + return crate::types::Or(false); + } - // Check all time slots are in range - if config.iter().any(|&slot| slot >= d) { - return false; - } + // Check all time slots are in range + if config.iter().any(|&slot| slot >= d) { + return crate::types::Or(false); + } - // Check processor capacity and resource constraints at each time slot - for u in 0..d { - // Collect tasks scheduled at time slot u - let mut task_count = 0usize; - let mut resource_usage = vec![0u64; r]; - - for (t, &slot) in config.iter().enumerate() { - if slot == u { - task_count += 1; - // Accumulate resource usage - for (usage, &req) in resource_usage - .iter_mut() - .zip(self.resource_requirements[t].iter()) - { - *usage = usage.saturating_add(req); + // Check processor capacity and resource constraints at each time slot + for u in 0..d { + // Collect tasks scheduled at time slot u + let mut task_count = 0usize; + let mut resource_usage = vec![0u64; r]; + + for (t, &slot) in config.iter().enumerate() { + if slot == u { + task_count += 1; + // Accumulate resource usage + for (usage, &req) in resource_usage + .iter_mut() + .zip(self.resource_requirements[t].iter()) + { + *usage = usage.saturating_add(req); + } } } - } - // Check processor capacity - if task_count > self.num_processors { - return false; - } + // Check processor capacity + if task_count > self.num_processors { + return crate::types::Or(false); + } - // Check resource bounds - for (usage, bound) in resource_usage.iter().zip(self.resource_bounds.iter()) { - if usage > bound { - return false; + // Check resource bounds + for (usage, bound) in resource_usage.iter().zip(self.resource_bounds.iter()) { + if usage > bound { + return crate::types::Or(false); + } } } - } - true + true + }) } } -impl SatisfactionProblem for ResourceConstrainedScheduling {} - crate::declare_variants! { - default sat ResourceConstrainedScheduling => "deadline ^ num_tasks", + default ResourceConstrainedScheduling => "deadline ^ num_tasks", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/scheduling_with_individual_deadlines.rs b/src/models/misc/scheduling_with_individual_deadlines.rs index 7a6629f17..391ca772b 100644 --- a/src/models/misc/scheduling_with_individual_deadlines.rs +++ b/src/models/misc/scheduling_with_individual_deadlines.rs @@ -5,7 +5,7 @@ //! every task finishes by its own deadline. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -102,7 +102,7 @@ impl SchedulingWithIndividualDeadlines { impl Problem for SchedulingWithIndividualDeadlines { const NAME: &'static str = "SchedulingWithIndividualDeadlines"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -112,40 +112,40 @@ impl Problem for SchedulingWithIndividualDeadlines { self.deadlines.clone() } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_tasks { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_tasks { + return crate::types::Or(false); + } - for (&start, &deadline) in config.iter().zip(&self.deadlines) { - if start >= deadline { - return false; + for (&start, &deadline) in config.iter().zip(&self.deadlines) { + if start >= deadline { + return crate::types::Or(false); + } } - } - for &(pred, succ) in &self.precedences { - if config[pred] + 1 > config[succ] { - return false; + for &(pred, succ) in &self.precedences { + if config[pred] + 1 > config[succ] { + return crate::types::Or(false); + } } - } - let mut slot_loads = BTreeMap::new(); - for &start in config { - let load = slot_loads.entry(start).or_insert(0usize); - *load += 1; - if *load > self.num_processors { - return false; + let mut slot_loads = BTreeMap::new(); + for &start in config { + let load = slot_loads.entry(start).or_insert(0usize); + *load += 1; + if *load > self.num_processors { + return crate::types::Or(false); + } } - } - true + true + }) } } -impl SatisfactionProblem for SchedulingWithIndividualDeadlines {} - crate::declare_variants! { - default sat SchedulingWithIndividualDeadlines => "max_deadline^num_tasks", + default SchedulingWithIndividualDeadlines => "max_deadline^num_tasks", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs b/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs index 4e8f94dd1..56a8e5136 100644 --- a/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs +++ b/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs @@ -5,7 +5,7 @@ //! cost never exceeds a given bound. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::de::Error as _; use serde::{Deserialize, Serialize}; @@ -153,7 +153,7 @@ fn precedence_validation_error(precedences: &[(usize, usize)], num_tasks: usize) impl Problem for SequencingToMinimizeMaximumCumulativeCost { const NAME: &'static str = "SequencingToMinimizeMaximumCumulativeCost"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -164,36 +164,36 @@ impl Problem for SequencingToMinimizeMaximumCumulativeCost { (0..n).rev().map(|i| i + 1).collect() } - fn evaluate(&self, config: &[usize]) -> bool { - let Some(schedule) = self.decode_schedule(config) else { - return false; - }; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let Some(schedule) = self.decode_schedule(config) else { + return crate::types::Or(false); + }; - let mut positions = vec![0usize; self.num_tasks()]; - for (position, &task) in schedule.iter().enumerate() { - positions[task] = position; - } - for &(pred, succ) in &self.precedences { - if positions[pred] >= positions[succ] { - return false; + let mut positions = vec![0usize; self.num_tasks()]; + for (position, &task) in schedule.iter().enumerate() { + positions[task] = position; + } + for &(pred, succ) in &self.precedences { + if positions[pred] >= positions[succ] { + return crate::types::Or(false); + } } - } - let mut cumulative = 0i64; - for &task in &schedule { - cumulative += self.costs[task]; - if cumulative > self.bound { - return false; + let mut cumulative = 0i64; + for &task in &schedule { + cumulative += self.costs[task]; + if cumulative > self.bound { + return crate::types::Or(false); + } } - } - true + true + }) } } -impl SatisfactionProblem for SequencingToMinimizeMaximumCumulativeCost {} - crate::declare_variants! { - default sat SequencingToMinimizeMaximumCumulativeCost => "factorial(num_tasks)", + default SequencingToMinimizeMaximumCumulativeCost => "factorial(num_tasks)", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs b/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs index 2f63c44b3..76cd0962f 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs @@ -6,8 +6,8 @@ //! weighted completion time. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -146,7 +146,7 @@ impl SequencingToMinimizeWeightedCompletionTime { Some(schedule) } - fn weighted_completion_time(&self, schedule: &[usize]) -> SolutionSize { + fn weighted_completion_time(&self, schedule: &[usize]) -> Min { let n = self.num_tasks(); let mut positions = vec![0usize; n]; let mut completion_times = vec![0u64; n]; @@ -162,7 +162,7 @@ impl SequencingToMinimizeWeightedCompletionTime { for &(pred, succ) in &self.precedences { if positions[pred] >= positions[succ] { - return SolutionSize::Invalid; + return Min(None); } } @@ -174,7 +174,7 @@ impl SequencingToMinimizeWeightedCompletionTime { acc.checked_add(weighted_completion) }) .expect("weighted completion time overflowed u64"); - SolutionSize::Valid(total) + Min(Some(total)) } } @@ -207,7 +207,7 @@ impl<'de> Deserialize<'de> for SequencingToMinimizeWeightedCompletionTime { impl Problem for SequencingToMinimizeWeightedCompletionTime { const NAME: &'static str = "SequencingToMinimizeWeightedCompletionTime"; - type Metric = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -218,24 +218,16 @@ impl Problem for SequencingToMinimizeWeightedCompletionTime { (0..n).rev().map(|i| i + 1).collect() } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let Some(schedule) = self.decode_schedule(config) else { - return SolutionSize::Invalid; + return Min(None); }; self.weighted_completion_time(&schedule) } } -impl OptimizationProblem for SequencingToMinimizeWeightedCompletionTime { - type Value = u64; - - fn direction(&self) -> Direction { - Direction::Minimize - } -} - crate::declare_variants! { - default opt SequencingToMinimizeWeightedCompletionTime => "factorial(num_tasks)", + default SequencingToMinimizeWeightedCompletionTime => "factorial(num_tasks)", } #[cfg(feature = "example-db")] @@ -248,7 +240,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -164,16 +164,16 @@ impl Problem for SequencingToMinimizeWeightedTardiness { (0..n).rev().map(|i| i + 1).collect() } - fn evaluate(&self, config: &[usize]) -> bool { - self.total_weighted_tardiness(config) - .is_some_and(|total| total <= self.bound) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + self.total_weighted_tardiness(config) + .is_some_and(|total| total <= self.bound) + }) } } -impl SatisfactionProblem for SequencingToMinimizeWeightedTardiness {} - crate::declare_variants! { - default sat SequencingToMinimizeWeightedTardiness => "factorial(num_tasks)", + default SequencingToMinimizeWeightedTardiness => "factorial(num_tasks)", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/sequencing_with_release_times_and_deadlines.rs b/src/models/misc/sequencing_with_release_times_and_deadlines.rs index 8a706a34f..f81bce5fe 100644 --- a/src/models/misc/sequencing_with_release_times_and_deadlines.rs +++ b/src/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -6,7 +6,7 @@ //! Strongly NP-complete (Garey & Johnson, A5 SS1). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -52,7 +52,7 @@ inventory::submit! { /// vec![3, 3, 4], /// ); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -106,7 +106,7 @@ impl SequencingWithReleaseTimesAndDeadlines { impl Problem for SequencingWithReleaseTimesAndDeadlines { const NAME: &'static str = "SequencingWithReleaseTimesAndDeadlines"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -117,41 +117,41 @@ impl Problem for SequencingWithReleaseTimesAndDeadlines { (0..n).rev().map(|i| i + 1).collect() } - fn evaluate(&self, config: &[usize]) -> bool { - let n = self.num_tasks(); - if config.len() != n { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n = self.num_tasks(); + if config.len() != n { + return crate::types::Or(false); + } - // Decode Lehmer code into a permutation of task indices. - let mut available: Vec = (0..n).collect(); - let mut schedule = Vec::with_capacity(n); - for &c in config.iter() { - if c >= available.len() { - return false; + // Decode Lehmer code into a permutation of task indices. + let mut available: Vec = (0..n).collect(); + let mut schedule = Vec::with_capacity(n); + for &c in config.iter() { + if c >= available.len() { + return crate::types::Or(false); + } + schedule.push(available.remove(c)); } - schedule.push(available.remove(c)); - } - // Schedule tasks left-to-right: each task starts at max(release_time, current_time). - let mut current_time: u64 = 0; - for &task in &schedule { - let start = current_time.max(self.release_times[task]); - let finish = start + self.lengths[task]; - if finish > self.deadlines[task] { - return false; + // Schedule tasks left-to-right: each task starts at max(release_time, current_time). + let mut current_time: u64 = 0; + for &task in &schedule { + let start = current_time.max(self.release_times[task]); + let finish = start + self.lengths[task]; + if finish > self.deadlines[task] { + return crate::types::Or(false); + } + current_time = finish; } - current_time = finish; - } - true + true + }) } } -impl SatisfactionProblem for SequencingWithReleaseTimesAndDeadlines {} - crate::declare_variants! { - default sat SequencingWithReleaseTimesAndDeadlines => "2^num_tasks * num_tasks", + default SequencingWithReleaseTimesAndDeadlines => "2^num_tasks * num_tasks", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/sequencing_within_intervals.rs b/src/models/misc/sequencing_within_intervals.rs index ed42ef47c..cc391444a 100644 --- a/src/models/misc/sequencing_within_intervals.rs +++ b/src/models/misc/sequencing_within_intervals.rs @@ -5,7 +5,7 @@ //! task runs entirely within its allowed time window. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -50,7 +50,7 @@ inventory::submit! { /// // 3 tasks: release_times = [0, 2, 4], deadlines = [3, 5, 7], lengths = [2, 2, 2] /// let problem = SequencingWithinIntervals::new(vec![0, 2, 4], vec![3, 5, 7], vec![2, 2, 2]); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -122,7 +122,7 @@ impl SequencingWithinIntervals { impl Problem for SequencingWithinIntervals { const NAME: &'static str = "SequencingWithinIntervals"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -134,45 +134,46 @@ impl Problem for SequencingWithinIntervals { .collect() } - fn evaluate(&self, config: &[usize]) -> bool { - let n = self.num_tasks(); - if config.len() != n { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n = self.num_tasks(); + if config.len() != n { + return crate::types::Or(false); + } - // Check each variable is within range and compute start times - let mut starts = Vec::with_capacity(n); - for (i, &c) in config.iter().enumerate() { - let dim = (self.deadlines[i] - self.release_times[i] - self.lengths[i] + 1) as usize; - if c >= dim { - return false; + // Check each variable is within range and compute start times + let mut starts = Vec::with_capacity(n); + for (i, &c) in config.iter().enumerate() { + let dim = + (self.deadlines[i] - self.release_times[i] - self.lengths[i] + 1) as usize; + if c >= dim { + return crate::types::Or(false); + } + // start = r[i] + c, and c < dim = d[i] - r[i] - l[i] + 1, + // so start + l[i] <= d[i] is guaranteed by construction. + let start = self.release_times[i] + c as u64; + starts.push(start); } - // start = r[i] + c, and c < dim = d[i] - r[i] - l[i] + 1, - // so start + l[i] <= d[i] is guaranteed by construction. - let start = self.release_times[i] + c as u64; - starts.push(start); - } - // Check no two tasks overlap - for i in 0..n { - for j in (i + 1)..n { - let end_i = starts[i] + self.lengths[i]; - let end_j = starts[j] + self.lengths[j]; - // Tasks overlap if neither finishes before the other starts - if !(end_i <= starts[j] || end_j <= starts[i]) { - return false; + // Check no two tasks overlap + for i in 0..n { + for j in (i + 1)..n { + let end_i = starts[i] + self.lengths[i]; + let end_j = starts[j] + self.lengths[j]; + // Tasks overlap if neither finishes before the other starts + if !(end_i <= starts[j] || end_j <= starts[i]) { + return crate::types::Or(false); + } } } - } - true + true + }) } } -impl SatisfactionProblem for SequencingWithinIntervals {} - crate::declare_variants! { - default sat SequencingWithinIntervals => "2^num_tasks", + default SequencingWithinIntervals => "2^num_tasks", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/shortest_common_supersequence.rs b/src/models/misc/shortest_common_supersequence.rs index b0a58641b..759b9efbc 100644 --- a/src/models/misc/shortest_common_supersequence.rs +++ b/src/models/misc/shortest_common_supersequence.rs @@ -11,7 +11,7 @@ //! to the standard `|w| ≤ B` formulation. This problem is NP-hard (Maier, 1978). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -52,7 +52,7 @@ inventory::submit! { /// // Alphabet {0, 1}, strings [0,1] and [1,0], bound 3 /// let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]], 3); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -125,7 +125,7 @@ fn is_subsequence(needle: &[usize], haystack: &[usize]) -> bool { impl Problem for ShortestCommonSupersequence { const NAME: &'static str = "ShortestCommonSupersequence"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -135,21 +135,21 @@ impl Problem for ShortestCommonSupersequence { vec![self.alphabet_size; self.bound] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.bound { - return false; - } - if config.iter().any(|&v| v >= self.alphabet_size) { - return false; - } - self.strings.iter().all(|s| is_subsequence(s, config)) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.bound { + return crate::types::Or(false); + } + if config.iter().any(|&v| v >= self.alphabet_size) { + return crate::types::Or(false); + } + self.strings.iter().all(|s| is_subsequence(s, config)) + }) } } -impl SatisfactionProblem for ShortestCommonSupersequence {} - crate::declare_variants! { - default sat ShortestCommonSupersequence => "alphabet_size ^ bound", + default ShortestCommonSupersequence => "alphabet_size ^ bound", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/stacker_crane.rs b/src/models/misc/stacker_crane.rs index b8786a4f1..40f8b27e4 100644 --- a/src/models/misc/stacker_crane.rs +++ b/src/models/misc/stacker_crane.rs @@ -5,7 +5,7 @@ //! traverses every required arc in some order and stays within the bound. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::cmp::Reverse; use std::collections::BinaryHeap; @@ -259,7 +259,7 @@ impl StackerCrane { impl Problem for StackerCrane { const NAME: &'static str = "StackerCrane"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -269,15 +269,15 @@ impl Problem for StackerCrane { vec![self.num_arcs(); self.num_arcs()] } - fn evaluate(&self, config: &[usize]) -> bool { - matches!(self.closed_walk_length(config), Some(total) if total <= self.bound) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + matches!(self.closed_walk_length(config), Some(total) if total <= self.bound) + }) } } -impl SatisfactionProblem for StackerCrane {} - crate::declare_variants! { - default sat StackerCrane => "num_vertices^2 * 2^num_arcs", + default StackerCrane => "num_vertices^2 * 2^num_arcs", } #[derive(Debug, Clone, Deserialize)] diff --git a/src/models/misc/staff_scheduling.rs b/src/models/misc/staff_scheduling.rs index e2f896493..7db6f75be 100644 --- a/src/models/misc/staff_scheduling.rs +++ b/src/models/misc/staff_scheduling.rs @@ -5,7 +5,7 @@ //! all requirements are met without exceeding the budget. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -150,19 +150,21 @@ impl StaffScheduling { impl Problem for StaffScheduling { const NAME: &'static str = "StaffScheduling"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.worker_limit() + 1; self.num_schedules()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_schedules() { - return false; - } - self.worker_counts_valid(config) - && self.within_budget(config) - && self.meets_requirements(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_schedules() { + return crate::types::Or(false); + } + self.worker_counts_valid(config) + && self.within_budget(config) + && self.meets_requirements(config) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -170,10 +172,8 @@ impl Problem for StaffScheduling { } } -impl SatisfactionProblem for StaffScheduling {} - crate::declare_variants! { - default sat StaffScheduling => "(num_workers + 1)^num_schedules", + default StaffScheduling => "(num_workers + 1)^num_schedules", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/string_to_string_correction.rs b/src/models/misc/string_to_string_correction.rs index 161de341c..30f02a3d0 100644 --- a/src/models/misc/string_to_string_correction.rs +++ b/src/models/misc/string_to_string_correction.rs @@ -15,7 +15,7 @@ //! This problem is NP-complete (Wagner, 1975). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -66,7 +66,7 @@ inventory::submit! { /// // source = [0,1,2,3,1,0], target = [0,1,3,2,1], bound = 2 /// let problem = StringToStringCorrection::new(4, vec![0,1,2,3,1,0], vec![0,1,3,2,1], 2); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -139,7 +139,7 @@ impl StringToStringCorrection { impl Problem for StringToStringCorrection { const NAME: &'static str = "StringToStringCorrection"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -149,49 +149,49 @@ impl Problem for StringToStringCorrection { vec![2 * self.source.len() + 1; self.bound] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.bound { - return false; - } - if self.target.len() > self.source.len() - || self.target.len() < self.source.len().saturating_sub(self.bound) - { - return false; - } - let n = self.source.len(); - let domain = 2 * n + 1; - if config.iter().any(|&v| v >= domain) { - return false; - } - let noop = 2 * n; - let mut working = self.source.clone(); - for &op in config { - if op == noop { - // no-op - continue; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.bound { + return crate::types::Or(false); + } + if self.target.len() > self.source.len() + || self.target.len() < self.source.len().saturating_sub(self.bound) + { + return crate::types::Or(false); } - let current_len = working.len(); - if op < current_len { - // delete at index op - working.remove(op); - } else { - let swap_pos = op - current_len; - if swap_pos + 1 < current_len { - working.swap(swap_pos, swap_pos + 1); + let n = self.source.len(); + let domain = 2 * n + 1; + if config.iter().any(|&v| v >= domain) { + return crate::types::Or(false); + } + let noop = 2 * n; + let mut working = self.source.clone(); + for &op in config { + if op == noop { + // no-op + continue; + } + let current_len = working.len(); + if op < current_len { + // delete at index op + working.remove(op); } else { - // invalid operation for current string state - return false; + let swap_pos = op - current_len; + if swap_pos + 1 < current_len { + working.swap(swap_pos, swap_pos + 1); + } else { + // invalid operation for current string state + return crate::types::Or(false); + } } } - } - working == self.target + working == self.target + }) } } -impl SatisfactionProblem for StringToStringCorrection {} - crate::declare_variants! { - default sat StringToStringCorrection => "(2 * source_length + 1) ^ bound", + default StringToStringCorrection => "(2 * source_length + 1) ^ bound", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/subset_sum.rs b/src/models/misc/subset_sum.rs index 7685cec22..873233e99 100644 --- a/src/models/misc/subset_sum.rs +++ b/src/models/misc/subset_sum.rs @@ -8,7 +8,7 @@ //! reductions can construct large instances without fixed-width overflow. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use num_bigint::{BigUint, ToBigUint}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -46,7 +46,7 @@ inventory::submit! { /// /// let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -108,7 +108,7 @@ impl SubsetSum { impl Problem for SubsetSum { const NAME: &'static str = "SubsetSum"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -118,27 +118,27 @@ impl Problem for SubsetSum { vec![2; self.num_elements()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_elements() { - return false; - } - if config.iter().any(|&v| v >= 2) { - return false; - } - let mut total = BigUint::zero(); - for (i, &x) in config.iter().enumerate() { - if x == 1 { - total += &self.sizes[i]; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_elements() { + return crate::types::Or(false); } - } - total == self.target + if config.iter().any(|&v| v >= 2) { + return crate::types::Or(false); + } + let mut total = BigUint::zero(); + for (i, &x) in config.iter().enumerate() { + if x == 1 { + total += &self.sizes[i]; + } + } + total == self.target + }) } } -impl SatisfactionProblem for SubsetSum {} - crate::declare_variants! { - default sat SubsetSum => "2^(num_elements / 2)", + default SubsetSum => "2^(num_elements / 2)", } mod decimal_biguint { diff --git a/src/models/misc/sum_of_squares_partition.rs b/src/models/misc/sum_of_squares_partition.rs index 0445d040e..c3768290f 100644 --- a/src/models/misc/sum_of_squares_partition.rs +++ b/src/models/misc/sum_of_squares_partition.rs @@ -6,7 +6,7 @@ //! NP-complete in the strong sense (Garey & Johnson, SP19). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::de::Error; use serde::{Deserialize, Deserializer, Serialize}; @@ -50,7 +50,7 @@ inventory::submit! { /// // 6 elements with sizes [5, 3, 8, 2, 7, 1], K=3 groups, bound J=240 /// let problem = SumOfSquaresPartition::new(vec![5, 3, 8, 2, 7, 1], 3, 240); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize)] @@ -164,7 +164,7 @@ impl<'de> Deserialize<'de> for SumOfSquaresPartition { impl Problem for SumOfSquaresPartition { const NAME: &'static str = "SumOfSquaresPartition"; - type Metric = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -174,18 +174,18 @@ impl Problem for SumOfSquaresPartition { vec![self.num_groups; self.sizes.len()] } - fn evaluate(&self, config: &[usize]) -> bool { - match self.sum_of_squares(config) { - Some(sos) => sos <= self.bound, - None => false, - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + match self.sum_of_squares(config) { + Some(sos) => sos <= self.bound, + None => false, + } + }) } } -impl SatisfactionProblem for SumOfSquaresPartition {} - crate::declare_variants! { - default sat SumOfSquaresPartition => "num_groups^num_elements", + default SumOfSquaresPartition => "num_groups^num_elements", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/timetable_design.rs b/src/models/misc/timetable_design.rs index f7678d9fa..ba290ce40 100644 --- a/src/models/misc/timetable_design.rs +++ b/src/models/misc/timetable_design.rs @@ -5,7 +5,7 @@ //! requirements. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -303,47 +303,51 @@ impl TimetableDesign { impl Problem for TimetableDesign { const NAME: &'static str = "TimetableDesign"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.config_len()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.config_len() { - return false; - } - if config.iter().any(|&value| value > 1) { - return false; - } - - let mut craftsman_busy = vec![vec![false; self.num_periods]; self.num_craftsmen]; - let mut task_busy = vec![vec![false; self.num_periods]; self.num_tasks]; - let mut pair_counts = vec![vec![0u64; self.num_tasks]; self.num_craftsmen]; - - for craftsman in 0..self.num_craftsmen { - for task in 0..self.num_tasks { - for period in 0..self.num_periods { - if config[self.index(craftsman, task, period)] == 0 { - continue; - } - - if !self.craftsman_avail[craftsman][period] || !self.task_avail[task][period] { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.config_len() { + return crate::types::Or(false); + } + if config.iter().any(|&value| value > 1) { + return crate::types::Or(false); + } - if craftsman_busy[craftsman][period] || task_busy[task][period] { - return false; + let mut craftsman_busy = vec![vec![false; self.num_periods]; self.num_craftsmen]; + let mut task_busy = vec![vec![false; self.num_periods]; self.num_tasks]; + let mut pair_counts = vec![vec![0u64; self.num_tasks]; self.num_craftsmen]; + + for craftsman in 0..self.num_craftsmen { + for task in 0..self.num_tasks { + for period in 0..self.num_periods { + if config[self.index(craftsman, task, period)] == 0 { + continue; + } + + if !self.craftsman_avail[craftsman][period] + || !self.task_avail[task][period] + { + return crate::types::Or(false); + } + + if craftsman_busy[craftsman][period] || task_busy[task][period] { + return crate::types::Or(false); + } + + craftsman_busy[craftsman][period] = true; + task_busy[task][period] = true; + pair_counts[craftsman][task] += 1; } - - craftsman_busy[craftsman][period] = true; - task_busy[task][period] = true; - pair_counts[craftsman][task] += 1; } } - } - pair_counts == self.requirements + pair_counts == self.requirements + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -351,10 +355,8 @@ impl Problem for TimetableDesign { } } -impl SatisfactionProblem for TimetableDesign {} - crate::declare_variants! { - default sat TimetableDesign => "2^(num_craftsmen * num_tasks * num_periods)", + default TimetableDesign => "2^(num_craftsmen * num_tasks * num_periods)", } #[cfg(any(test, feature = "example-db"))] diff --git a/src/models/set/comparative_containment.rs b/src/models/set/comparative_containment.rs index 50a80beb9..4076925da 100644 --- a/src/models/set/comparative_containment.rs +++ b/src/models/set/comparative_containment.rs @@ -5,7 +5,7 @@ //! in the first family is at least its containment weight in the second. use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use crate::types::{One, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -177,14 +177,14 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "ComparativeContainment"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.universe_size] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -192,15 +192,10 @@ where } } -impl SatisfactionProblem for ComparativeContainment where - W: WeightElement + crate::variant::VariantParam -{ -} - crate::declare_variants! { - sat ComparativeContainment => "2^universe_size", - default sat ComparativeContainment => "2^universe_size", - sat ComparativeContainment => "2^universe_size", + ComparativeContainment => "2^universe_size", + default ComparativeContainment => "2^universe_size", + ComparativeContainment => "2^universe_size", } fn validate_set_family(label: &str, universe_size: usize, sets: &[Vec]) { diff --git a/src/models/set/consecutive_sets.rs b/src/models/set/consecutive_sets.rs index c787d4d56..1e50f29dc 100644 --- a/src/models/set/consecutive_sets.rs +++ b/src/models/set/consecutive_sets.rs @@ -6,7 +6,7 @@ //! contiguous block in some order) within the string. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -55,7 +55,7 @@ inventory::submit! { /// ); /// /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// /// // w = [0, 4, 2, 5, 1, 3] is a valid solution /// assert!(solution.is_some()); @@ -135,83 +135,85 @@ impl ConsecutiveSets { impl Problem for ConsecutiveSets { const NAME: &'static str = "ConsecutiveSets"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { // Each position can be any symbol (0..alphabet_size-1) or "unused" (alphabet_size) vec![self.alphabet_size + 1; self.bound_k] } - fn evaluate(&self, config: &[usize]) -> bool { - // 1. Validate config - if config.len() != self.bound_k || config.iter().any(|&v| v > self.alphabet_size) { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + // 1. Validate config + if config.len() != self.bound_k || config.iter().any(|&v| v > self.alphabet_size) { + return crate::types::Or(false); + } - // 2. Build string: find the actual string length (strip trailing "unused") - let unused = self.alphabet_size; - let str_len = config - .iter() - .rposition(|&v| v != unused) - .map_or(0, |p| p + 1); - - // 3. Check no internal "unused" symbols - let w = &config[..str_len]; - if w.contains(&unused) { - return false; - } + // 2. Build string: find the actual string length (strip trailing "unused") + let unused = self.alphabet_size; + let str_len = config + .iter() + .rposition(|&v| v != unused) + .map_or(0, |p| p + 1); + + // 3. Check no internal "unused" symbols + let w = &config[..str_len]; + if w.contains(&unused) { + return crate::types::Or(false); + } - let mut subset_membership = vec![0usize; self.alphabet_size]; - let mut seen_in_window = vec![0usize; self.alphabet_size]; - let mut subset_stamp = 1usize; - let mut window_stamp = 1usize; + let mut subset_membership = vec![0usize; self.alphabet_size]; + let mut seen_in_window = vec![0usize; self.alphabet_size]; + let mut subset_stamp = 1usize; + let mut window_stamp = 1usize; - // 4. Check each subset has a consecutive block - for subset in &self.subsets { - let subset_len = subset.len(); - if subset_len == 0 { - continue; // empty subset trivially satisfied - } - if subset_len > str_len { - return false; // can't fit - } + // 4. Check each subset has a consecutive block + for subset in &self.subsets { + let subset_len = subset.len(); + if subset_len == 0 { + continue; // empty subset trivially satisfied + } + if subset_len > str_len { + return crate::types::Or(false); // can't fit + } - for &elem in subset { - subset_membership[elem] = subset_stamp; - } + for &elem in subset { + subset_membership[elem] = subset_stamp; + } - let mut found = false; - for start in 0..=(str_len - subset_len) { - let window = &w[start..start + subset_len]; - let current_window_stamp = window_stamp; - window_stamp += 1; - - // Because subsets are validated to contain unique elements, - // a window matches iff every symbol belongs to the subset and - // appears at most once. - if window.iter().all(|&elem| { - let is_member = subset_membership[elem] == subset_stamp; - let is_new = seen_in_window[elem] != current_window_stamp; - if is_member && is_new { - seen_in_window[elem] = current_window_stamp; - true - } else { - false + let mut found = false; + for start in 0..=(str_len - subset_len) { + let window = &w[start..start + subset_len]; + let current_window_stamp = window_stamp; + window_stamp += 1; + + // Because subsets are validated to contain unique elements, + // a window matches iff every symbol belongs to the subset and + // appears at most once. + if window.iter().all(|&elem| { + let is_member = subset_membership[elem] == subset_stamp; + let is_new = seen_in_window[elem] != current_window_stamp; + if is_member && is_new { + seen_in_window[elem] = current_window_stamp; + true + } else { + false + } + }) { + // subset is already sorted + found = true; + break; } - }) { - // subset is already sorted - found = true; - break; } - } - if !found { - return false; - } + if !found { + return crate::types::Or(false); + } - subset_stamp += 1; - } + subset_stamp += 1; + } - true + true + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -219,10 +221,8 @@ impl Problem for ConsecutiveSets { } } -impl SatisfactionProblem for ConsecutiveSets {} - crate::declare_variants! { - default sat ConsecutiveSets => "alphabet_size^bound_k * num_subsets", + default ConsecutiveSets => "alphabet_size^bound_k * num_subsets", } #[cfg(feature = "example-db")] diff --git a/src/models/set/exact_cover_by_3_sets.rs b/src/models/set/exact_cover_by_3_sets.rs index 0bf0fdf36..62fa875ce 100644 --- a/src/models/set/exact_cover_by_3_sets.rs +++ b/src/models/set/exact_cover_by_3_sets.rs @@ -5,7 +5,7 @@ //! q disjoint triples covering every element exactly once. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -47,7 +47,7 @@ inventory::submit! { /// ); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_satisfying(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // S0 and S1 form an exact cover /// assert_eq!(solutions.len(), 1); @@ -124,7 +124,7 @@ impl ExactCoverBy3Sets { /// A valid exact cover selects exactly q = universe_size/3 subsets /// that are pairwise disjoint and whose union equals the universe. pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } /// Get the elements covered by the selected subsets. @@ -143,42 +143,44 @@ impl ExactCoverBy3Sets { impl Problem for ExactCoverBy3Sets { const NAME: &'static str = "ExactCoverBy3Sets"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.subsets.len()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.subsets.len() || config.iter().any(|&value| value > 1) { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.subsets.len() || config.iter().any(|&value| value > 1) { + return crate::types::Or(false); + } - let q = self.universe_size / 3; + let q = self.universe_size / 3; - // Count selected subsets - let selected_count: usize = config.iter().filter(|&&v| v == 1).sum(); - if selected_count != q { - return false; - } + // Count selected subsets + let selected_count: usize = config.iter().filter(|&&v| v == 1).sum(); + if selected_count != q { + return crate::types::Or(false); + } - // Check that selected subsets are pairwise disjoint and cover everything - let mut covered = HashSet::with_capacity(self.universe_size); - for (i, &selected) in config.iter().enumerate() { - if selected == 1 { - if let Some(subset) = self.subsets.get(i) { - for &elem in subset { - if !covered.insert(elem) { - // Element already covered -- not disjoint - return false; + // Check that selected subsets are pairwise disjoint and cover everything + let mut covered = HashSet::with_capacity(self.universe_size); + for (i, &selected) in config.iter().enumerate() { + if selected == 1 { + if let Some(subset) = self.subsets.get(i) { + for &elem in subset { + if !covered.insert(elem) { + // Element already covered -- not disjoint + return crate::types::Or(false); + } } } } } - } - // Check all elements are covered - covered.len() == self.universe_size + // Check all elements are covered + covered.len() == self.universe_size + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -186,10 +188,8 @@ impl Problem for ExactCoverBy3Sets { } } -impl SatisfactionProblem for ExactCoverBy3Sets {} - crate::declare_variants! { - default sat ExactCoverBy3Sets => "2^universe_size", + default ExactCoverBy3Sets => "2^universe_size", } #[cfg(feature = "example-db")] diff --git a/src/models/set/maximum_set_packing.rs b/src/models/set/maximum_set_packing.rs index 45aff086a..fb199d5a2 100644 --- a/src/models/set/maximum_set_packing.rs +++ b/src/models/set/maximum_set_packing.rs @@ -4,8 +4,8 @@ //! pairwise disjoint sets. use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, One, SolutionSize, WeightElement}; +use crate::traits::Problem; +use crate::types::{Max, One, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -46,7 +46,7 @@ inventory::submit! { /// ]); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Verify solutions are pairwise disjoint /// for sol in solutions { @@ -141,15 +141,15 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MaximumSetPacking"; - type Metric = SolutionSize; + type Value = Max; fn dims(&self) -> Vec { vec![2; self.sets.len()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if !is_valid_packing(&self.sets, config) { - return SolutionSize::Invalid; + return Max(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -157,7 +157,7 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) + Max(Some(total)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -165,21 +165,10 @@ where } } -impl OptimizationProblem for MaximumSetPacking -where - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Maximize - } -} - crate::declare_variants! { - default opt MaximumSetPacking => "2^num_sets", - opt MaximumSetPacking => "2^num_sets", - opt MaximumSetPacking => "2^num_sets", + default MaximumSetPacking => "2^num_sets", + MaximumSetPacking => "2^num_sets", + MaximumSetPacking => "2^num_sets", } /// Check if a selection forms a valid set packing (pairwise disjoint). @@ -225,7 +214,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec { vec![2; self.num_attributes] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_attributes || config.iter().any(|&v| v > 1) { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_attributes || config.iter().any(|&v| v > 1) { + return crate::types::Or(false); + } - let selected: Vec = config.iter().map(|&v| v == 1).collect(); - let count = selected.iter().filter(|&&v| v).count(); + let selected: Vec = config.iter().map(|&v| v == 1).collect(); + let count = selected.iter().filter(|&&v| v).count(); - if (count as i64) > self.bound { - return false; - } + if (count as i64) > self.bound { + return crate::types::Or(false); + } - self.is_minimal_key(&selected) + self.is_minimal_key(&selected) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -174,10 +176,8 @@ impl Problem for MinimumCardinalityKey { } } -impl SatisfactionProblem for MinimumCardinalityKey {} - crate::declare_variants! { - default sat MinimumCardinalityKey => "2^num_attributes", + default MinimumCardinalityKey => "2^num_attributes", } #[cfg(feature = "example-db")] diff --git a/src/models/set/minimum_hitting_set.rs b/src/models/set/minimum_hitting_set.rs index 06c362382..e7b0d47d2 100644 --- a/src/models/set/minimum_hitting_set.rs +++ b/src/models/set/minimum_hitting_set.rs @@ -4,8 +4,8 @@ //! elements that intersects every set in a collection. use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -117,24 +117,24 @@ impl MinimumHittingSet { impl Problem for MinimumHittingSet { const NAME: &'static str = "MinimumHittingSet"; - type Metric = SolutionSize; + type Value = Min; fn dims(&self) -> Vec { vec![2; self.universe_size] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let Some(selected) = self.selected_elements(config) else { - return SolutionSize::Invalid; + return Min(None); }; if self.sets.iter().all(|set| { set.iter() .any(|element| selected.binary_search(element).is_ok()) }) { - SolutionSize::Valid(selected.len()) + Min(Some(selected.len())) } else { - SolutionSize::Invalid + Min(None) } } @@ -143,16 +143,8 @@ impl Problem for MinimumHittingSet { } } -impl OptimizationProblem for MinimumHittingSet { - type Value = usize; - - fn direction(&self) -> Direction { - Direction::Minimize - } -} - crate::declare_variants! { - default opt MinimumHittingSet => "2^universe_size", + default MinimumHittingSet => "2^universe_size", } #[cfg(feature = "example-db")] @@ -172,7 +164,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn dims(&self) -> Vec { vec![2; self.sets.len()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let covered = self.covered_elements(config); let is_valid = covered.len() == self.universe_size && (0..self.universe_size).all(|e| covered.contains(&e)); if !is_valid { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -162,7 +162,7 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) + Min(Some(total)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -170,19 +170,8 @@ where } } -impl OptimizationProblem for MinimumSetCovering -where - W: WeightElement + crate::variant::VariantParam, -{ - type Value = W::Sum; - - fn direction(&self) -> Direction { - Direction::Minimize - } -} - crate::declare_variants! { - default opt MinimumSetCovering => "2^num_sets", + default MinimumSetCovering => "2^num_sets", } /// Check if a selection of sets forms a valid set cover. @@ -211,7 +200,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -57,7 +57,7 @@ inventory::submit! { /// assert!(problem.evaluate(&[0, 0, 1, 1, 0, 0])); /// /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -155,46 +155,48 @@ impl PrimeAttributeName { impl Problem for PrimeAttributeName { const NAME: &'static str = "PrimeAttributeName"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.num_attributes] } - fn evaluate(&self, config: &[usize]) -> bool { - // Check config length and binary values - if config.len() != self.num_attributes || config.iter().any(|&v| v > 1) { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + // Check config length and binary values + if config.len() != self.num_attributes || config.iter().any(|&v| v > 1) { + return crate::types::Or(false); + } - // K = {i : config[i] = 1} - let k: Vec = config.iter().map(|&v| v == 1).collect(); + // K = {i : config[i] = 1} + let k: Vec = config.iter().map(|&v| v == 1).collect(); - // query_attribute must be in K - if !k[self.query_attribute] { - return false; - } + // query_attribute must be in K + if !k[self.query_attribute] { + return crate::types::Or(false); + } - // Compute closure(K) -- must equal all attributes (K is a superkey) - let closure = self.compute_closure(&k); - if closure.iter().any(|&v| !v) { - return false; - } + // Compute closure(K) -- must equal all attributes (K is a superkey) + let closure = self.compute_closure(&k); + if closure.iter().any(|&v| !v) { + return crate::types::Or(false); + } - // Check minimality: removing any attribute from K must break the superkey property - for i in 0..self.num_attributes { - if k[i] { - let mut reduced = k.clone(); - reduced[i] = false; - let reduced_closure = self.compute_closure(&reduced); - if reduced_closure.iter().all(|&v| v) { - // K \ {i} is still a superkey, so K is not minimal - return false; + // Check minimality: removing any attribute from K must break the superkey property + for i in 0..self.num_attributes { + if k[i] { + let mut reduced = k.clone(); + reduced[i] = false; + let reduced_closure = self.compute_closure(&reduced); + if reduced_closure.iter().all(|&v| v) { + // K \ {i} is still a superkey, so K is not minimal + return crate::types::Or(false); + } } } - } - true + true + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -202,10 +204,8 @@ impl Problem for PrimeAttributeName { } } -impl SatisfactionProblem for PrimeAttributeName {} - crate::declare_variants! { - default sat PrimeAttributeName => "2^num_attributes * num_dependencies * num_attributes", + default PrimeAttributeName => "2^num_attributes * num_dependencies * num_attributes", } #[cfg(feature = "example-db")] diff --git a/src/models/set/rooted_tree_storage_assignment.rs b/src/models/set/rooted_tree_storage_assignment.rs index 9692fcc84..b4138f5af 100644 --- a/src/models/set/rooted_tree_storage_assignment.rs +++ b/src/models/set/rooted_tree_storage_assignment.rs @@ -1,7 +1,7 @@ //! Rooted Tree Storage Assignment problem implementation. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -174,39 +174,41 @@ impl RootedTreeStorageAssignment { impl Problem for RootedTreeStorageAssignment { const NAME: &'static str = "RootedTreeStorageAssignment"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.universe_size; self.universe_size] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.universe_size { - return false; - } - if config.iter().any(|&parent| parent >= self.universe_size) { - return false; - } - if self.universe_size == 0 { - return self.subsets.is_empty(); - } - - let Some(depth) = Self::analyze_tree(config) else { - return false; - }; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.universe_size { + return crate::types::Or(false); + } + if config.iter().any(|&parent| parent >= self.universe_size) { + return crate::types::Or(false); + } + if self.universe_size == 0 { + return crate::types::Or(self.subsets.is_empty()); + } - let mut total_cost = 0usize; - for subset in &self.subsets { - let Some(cost) = self.subset_extension_cost(subset, config, &depth) else { - return false; + let Some(depth) = Self::analyze_tree(config) else { + return crate::types::Or(false); }; - total_cost += cost; - if total_cost > self.bound { - return false; + + let mut total_cost = 0usize; + for subset in &self.subsets { + let Some(cost) = self.subset_extension_cost(subset, config, &depth) else { + return crate::types::Or(false); + }; + total_cost += cost; + if total_cost > self.bound { + return crate::types::Or(false); + } } - } - true + true + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -214,10 +216,8 @@ impl Problem for RootedTreeStorageAssignment { } } -impl SatisfactionProblem for RootedTreeStorageAssignment {} - crate::declare_variants! { - default sat RootedTreeStorageAssignment => "universe_size^universe_size", + default RootedTreeStorageAssignment => "universe_size^universe_size", } #[cfg(feature = "example-db")] diff --git a/src/models/set/set_basis.rs b/src/models/set/set_basis.rs index 0d0cd8dbb..e1b9f92bf 100644 --- a/src/models/set/set_basis.rs +++ b/src/models/set/set_basis.rs @@ -5,7 +5,7 @@ //! can be reconstructed as a union of some subcollection of the basis. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -96,7 +96,7 @@ impl SetBasis { /// Check whether the configuration is a satisfying Set Basis solution. pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } fn decode_basis(&self, config: &[usize]) -> Option>> { @@ -147,20 +147,22 @@ impl SetBasis { impl Problem for SetBasis { const NAME: &'static str = "SetBasis"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.k * self.universe_size] } - fn evaluate(&self, config: &[usize]) -> bool { - let Some(basis) = self.decode_basis(config) else { - return false; - }; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let Some(basis) = self.decode_basis(config) else { + return crate::types::Or(false); + }; - self.collection - .iter() - .all(|target| Self::can_represent_target(&basis, target, self.universe_size)) + self.collection + .iter() + .all(|target| Self::can_represent_target(&basis, target, self.universe_size)) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -168,10 +170,8 @@ impl Problem for SetBasis { } } -impl SatisfactionProblem for SetBasis {} - crate::declare_variants! { - default sat SetBasis => "2^(basis_size * universe_size)", + default SetBasis => "2^(basis_size * universe_size)", } #[cfg(feature = "example-db")] diff --git a/src/models/set/two_dimensional_consecutive_sets.rs b/src/models/set/two_dimensional_consecutive_sets.rs index a3e6b20ac..de247c110 100644 --- a/src/models/set/two_dimensional_consecutive_sets.rs +++ b/src/models/set/two_dimensional_consecutive_sets.rs @@ -6,7 +6,7 @@ //! are spread across consecutive groups. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; use serde::de::Error as _; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -145,55 +145,57 @@ impl TwoDimensionalConsecutiveSets { impl Problem for TwoDimensionalConsecutiveSets { const NAME: &'static str = "TwoDimensionalConsecutiveSets"; - type Metric = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.alphabet_size; self.alphabet_size] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.alphabet_size { - return false; - } - if config.iter().any(|&v| v >= self.alphabet_size) { - return false; - } - - // Empty labels do not create gaps in the partition order, so compress used labels first. - let mut used = vec![false; self.alphabet_size]; - for &group in config { - used[group] = true; - } - let mut dense_labels = vec![0; self.alphabet_size]; - let mut next_label = 0; - for (label, is_used) in used.into_iter().enumerate() { - if is_used { - dense_labels[label] = next_label; - next_label += 1; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.alphabet_size { + return crate::types::Or(false); } - } - - for subset in &self.subsets { - if subset.is_empty() { - continue; + if config.iter().any(|&v| v >= self.alphabet_size) { + return crate::types::Or(false); } - let groups: Vec = subset.iter().map(|&s| dense_labels[config[s]]).collect(); - // Intersection constraint: all group indices must be distinct - let unique: HashSet = groups.iter().copied().collect(); - if unique.len() != subset.len() { - return false; + // Empty labels do not create gaps in the partition order, so compress used labels first. + let mut used = vec![false; self.alphabet_size]; + for &group in config { + used[group] = true; + } + let mut dense_labels = vec![0; self.alphabet_size]; + let mut next_label = 0; + for (label, is_used) in used.into_iter().enumerate() { + if is_used { + dense_labels[label] = next_label; + next_label += 1; + } } - // Consecutiveness: group indices must form a contiguous range - let min_g = *unique.iter().min().unwrap(); - let max_g = *unique.iter().max().unwrap(); - if max_g - min_g + 1 != subset.len() { - return false; + for subset in &self.subsets { + if subset.is_empty() { + continue; + } + let groups: Vec = subset.iter().map(|&s| dense_labels[config[s]]).collect(); + + // Intersection constraint: all group indices must be distinct + let unique: HashSet = groups.iter().copied().collect(); + if unique.len() != subset.len() { + return crate::types::Or(false); + } + + // Consecutiveness: group indices must form a contiguous range + let min_g = *unique.iter().min().unwrap(); + let max_g = *unique.iter().max().unwrap(); + if max_g - min_g + 1 != subset.len() { + return crate::types::Or(false); + } } - } - true + true + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -201,10 +203,8 @@ impl Problem for TwoDimensionalConsecutiveSets { } } -impl SatisfactionProblem for TwoDimensionalConsecutiveSets {} - crate::declare_variants! { - default sat TwoDimensionalConsecutiveSets => "alphabet_size^alphabet_size", + default TwoDimensionalConsecutiveSets => "alphabet_size^alphabet_size", } #[cfg(feature = "example-db")] diff --git a/src/registry/dyn_problem.rs b/src/registry/dyn_problem.rs index 6b1a9b0e0..19483463a 100644 --- a/src/registry/dyn_problem.rs +++ b/src/registry/dyn_problem.rs @@ -6,11 +6,23 @@ use std::fmt; use crate::traits::Problem; +/// Format a metric for CLI- and registry-facing dynamic dispatch. +/// +/// Dynamic formatting uses the aggregate display form directly, so optimization +/// metrics appear as `Max(...)` / `Min(...)` alongside aggregate-only values +/// such as `Or(true)` or `Sum(56)`. +pub fn format_metric(metric: &T) -> String +where + T: fmt::Display, +{ + metric.to_string() +} + /// Type-erased problem interface for dynamic dispatch. /// /// Implemented via blanket impl for any `T: Problem + Serialize + 'static`. pub trait DynProblem: Any { - /// Evaluate a configuration and return the result as a debug string. + /// Evaluate a configuration and return the CLI-facing metric string. fn evaluate_dyn(&self, config: &[usize]) -> String; /// Evaluate a configuration and return the result as a serializable JSON value. fn evaluate_json(&self, config: &[usize]) -> Value; @@ -31,10 +43,10 @@ pub trait DynProblem: Any { impl DynProblem for T where T: Problem + Serialize + 'static, - T::Metric: fmt::Debug + Serialize, + T::Value: fmt::Display + Serialize, { fn evaluate_dyn(&self, config: &[usize]) -> String { - format!("{:?}", self.evaluate(config)) + format_metric(&self.evaluate(config)) } fn evaluate_json(&self, config: &[usize]) -> Value { @@ -66,15 +78,19 @@ where } } -/// Function pointer type for brute-force solve dispatch. -pub type SolveFn = fn(&dyn Any) -> Option<(Vec, String)>; +/// Function pointer type for brute-force value solve dispatch. +pub type SolveValueFn = fn(&dyn Any) -> String; + +/// Function pointer type for brute-force witness solve dispatch. +pub type SolveWitnessFn = fn(&dyn Any) -> Option<(Vec, String)>; /// A loaded problem with type-erased solve capability. /// -/// Wraps a `Box` with a brute-force solve function pointer. +/// Wraps a `Box` with brute-force value and witness function pointers. pub struct LoadedDynProblem { inner: Box, - solve_fn: SolveFn, + solve_value_fn: SolveValueFn, + solve_witness_fn: SolveWitnessFn, } impl std::fmt::Debug for LoadedDynProblem { @@ -87,13 +103,31 @@ impl std::fmt::Debug for LoadedDynProblem { impl LoadedDynProblem { /// Create a new loaded dynamic problem. - pub fn new(inner: Box, solve_fn: SolveFn) -> Self { - Self { inner, solve_fn } + pub fn new( + inner: Box, + solve_value_fn: SolveValueFn, + solve_witness_fn: SolveWitnessFn, + ) -> Self { + Self { + inner, + solve_value_fn, + solve_witness_fn, + } + } + + /// Solve the problem using brute force and return its aggregate value string. + pub fn solve_brute_force_value(&self) -> String { + (self.solve_value_fn)(self.inner.as_any()) + } + + /// Solve the problem using brute force and return a witness when available. + pub fn solve_brute_force_witness(&self) -> Option<(Vec, String)> { + (self.solve_witness_fn)(self.inner.as_any()) } - /// Solve the problem using brute force. + /// Backward-compatible witness solve entry point. pub fn solve_brute_force(&self) -> Option<(Vec, String)> { - (self.solve_fn)(self.inner.as_any()) + self.solve_brute_force_witness() } } diff --git a/src/registry/mod.rs b/src/registry/mod.rs index d72609729..24e6271c2 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -51,7 +51,7 @@ pub mod problem_type; mod schema; pub mod variant; -pub use dyn_problem::{DynProblem, LoadedDynProblem, SolveFn}; +pub use dyn_problem::{format_metric, DynProblem, LoadedDynProblem, SolveValueFn, SolveWitnessFn}; pub use info::{ComplexityClass, FieldInfo, ProblemInfo, ProblemMetadata}; pub use problem_ref::{parse_catalog_problem_ref, require_graph_variant, ProblemRef}; pub use problem_type::{find_problem_type, find_problem_type_by_alias, problem_types, ProblemType}; @@ -81,7 +81,11 @@ pub fn load_dyn( let inner = (entry.factory)(data).map_err(|e| format!("Failed to deserialize `{name}`: {e}"))?; - Ok(LoadedDynProblem::new(inner, entry.solve_fn)) + Ok(LoadedDynProblem::new( + inner, + entry.solve_value_fn, + entry.solve_witness_fn, + )) } /// Serialize a `&dyn Any` by exact problem name and exact variant map. diff --git a/src/registry/variant.rs b/src/registry/variant.rs index a4e6fd35a..4bca0369e 100644 --- a/src/registry/variant.rs +++ b/src/registry/variant.rs @@ -3,7 +3,7 @@ use std::any::Any; use std::collections::BTreeMap; -use crate::registry::dyn_problem::{DynProblem, SolveFn}; +use crate::registry::dyn_problem::{DynProblem, SolveValueFn, SolveWitnessFn}; /// A registered problem variant entry. /// @@ -26,8 +26,10 @@ pub struct VariantEntry { pub factory: fn(serde_json::Value) -> Result, serde_json::Error>, /// Serialize: downcast `&dyn Any` and serialize to JSON. pub serialize_fn: fn(&dyn Any) -> Option, - /// Solve: downcast `&dyn Any` and brute-force solve. - pub solve_fn: SolveFn, + /// Solve value: downcast `&dyn Any` and brute-force solve to an aggregate string. + pub solve_value_fn: SolveValueFn, + /// Solve witness: downcast `&dyn Any` and brute-force recover a witness when available. + pub solve_witness_fn: SolveWitnessFn, } impl VariantEntry { diff --git a/src/rules/graph.rs b/src/rules/graph.rs index fff45b23b..303cbb698 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -13,12 +13,14 @@ //! - JSON export for documentation and visualization use crate::rules::cost::PathCostFn; -use crate::rules::registry::{ReductionEntry, ReductionOverhead}; -use crate::rules::traits::DynReductionResult; +use crate::rules::registry::{ + AggregateReduceFn, EdgeCapabilities, ReduceFn, ReductionEntry, ReductionOverhead, +}; +use crate::rules::traits::{DynAggregateReductionResult, DynReductionResult}; use crate::types::ProblemSize; use ordered_float::OrderedFloat; use petgraph::algo::all_simple_paths; -use petgraph::graph::{DiGraph, NodeIndex}; +use petgraph::graph::{DiGraph, EdgeIndex, NodeIndex}; use petgraph::visit::EdgeRef; use serde::Serialize; use std::any::Any; @@ -34,13 +36,16 @@ pub struct ReductionEdgeInfo { pub target_name: &'static str, pub target_variant: BTreeMap, pub overhead: ReductionOverhead, + pub capabilities: EdgeCapabilities, } /// Internal edge data combining overhead and executable reduce function. #[derive(Clone)] pub(crate) struct ReductionEdgeData { pub overhead: ReductionOverhead, - pub reduce_fn: fn(&dyn Any) -> Box, + pub reduce_fn: Option, + pub reduce_aggregate_fn: Option, + pub capabilities: EdgeCapabilities, } /// JSON-serializable representation of the reduction graph. @@ -108,6 +113,10 @@ pub(crate) struct EdgeJson { pub(crate) overhead: Vec, /// Relative rustdoc path for the reduction module. pub(crate) doc_path: String, + /// Whether the edge supports witness/config workflows. + pub(crate) witness: bool, + /// Whether the edge supports aggregate/value workflows. + pub(crate) aggregate: bool, } /// A path through the variant-level reduction graph. @@ -228,9 +237,9 @@ pub struct NeighborInfo { pub hops: usize, } -/// Direction for graph traversal. +/// Traversal mode for graph exploration. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TraversalDirection { +pub enum TraversalFlow { /// Follow outgoing edges (what can this reduce to?). Outgoing, /// Follow incoming edges (what can reduce to this?). @@ -239,6 +248,13 @@ pub enum TraversalDirection { Both, } +/// Required capability for reduction path search. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReductionMode { + Witness, + Aggregate, +} + /// A tree node for neighbor traversal results. #[derive(Debug, Clone)] pub struct NeighborTree { @@ -361,6 +377,8 @@ impl ReductionGraph { ReductionEdgeData { overhead, reduce_fn: entry.reduce_fn, + reduce_aggregate_fn: entry.reduce_aggregate_fn, + capabilities: entry.capabilities, }, ); } @@ -399,6 +417,21 @@ impl ReductionGraph { .copied() } + fn edge_supports_mode(edge: &ReductionEdgeData, mode: ReductionMode) -> bool { + match mode { + ReductionMode::Witness => edge.capabilities.witness, + ReductionMode::Aggregate => edge.capabilities.aggregate, + } + } + + fn node_path_supports_mode(&self, node_path: &[NodeIndex], mode: ReductionMode) -> bool { + node_path.windows(2).all(|pair| { + self.graph + .find_edge(pair[0], pair[1]) + .is_some_and(|edge_idx| Self::edge_supports_mode(&self.graph[edge_idx], mode)) + }) + } + /// Find the cheapest path between two specific problem variants. /// /// Uses Dijkstra's algorithm on the variant-level graph from the exact @@ -411,10 +444,34 @@ impl ReductionGraph { target_variant: &BTreeMap, input_size: &ProblemSize, cost_fn: &C, + ) -> Option { + self.find_cheapest_path_mode( + source, + source_variant, + target, + target_variant, + ReductionMode::Witness, + input_size, + cost_fn, + ) + } + + /// Find the cheapest path between two specific problem variants while + /// requiring a specific edge capability. + #[allow(clippy::too_many_arguments)] + pub fn find_cheapest_path_mode( + &self, + source: &str, + source_variant: &BTreeMap, + target: &str, + target_variant: &BTreeMap, + mode: ReductionMode, + input_size: &ProblemSize, + cost_fn: &C, ) -> Option { let src = self.lookup_node(source, source_variant)?; let dst = self.lookup_node(target, target_variant)?; - let node_path = self.dijkstra(src, dst, input_size, cost_fn)?; + let node_path = self.dijkstra(src, dst, mode, input_size, cost_fn)?; Some(self.node_path_to_reduction_path(&node_path)) } @@ -423,6 +480,7 @@ impl ReductionGraph { &self, src: NodeIndex, dst: NodeIndex, + mode: ReductionMode, input_size: &ProblemSize, cost_fn: &C, ) -> Option> { @@ -458,6 +516,9 @@ impl ReductionGraph { }; for edge_ref in self.graph.edges(node) { + if !Self::edge_supports_mode(edge_ref.weight(), mode) { + continue; + } let overhead = &edge_ref.weight().overhead; let next = edge_ref.target(); @@ -502,6 +563,25 @@ impl ReductionGraph { source_variant: &BTreeMap, target: &str, target_variant: &BTreeMap, + ) -> Vec { + self.find_all_paths_mode( + source, + source_variant, + target, + target_variant, + ReductionMode::Witness, + ) + } + + /// Find all simple paths between two specific problem variants while + /// requiring a specific edge capability. + pub fn find_all_paths_mode( + &self, + source: &str, + source_variant: &BTreeMap, + target: &str, + target_variant: &BTreeMap, + mode: ReductionMode, ) -> Vec { let src = match self.lookup_node(source, source_variant) { Some(idx) => idx, @@ -521,6 +601,7 @@ impl ReductionGraph { paths .iter() + .filter(|p| self.node_path_supports_mode(p, mode)) .map(|p| self.node_path_to_reduction_path(p)) .collect() } @@ -536,6 +617,27 @@ impl ReductionGraph { target: &str, target_variant: &BTreeMap, limit: usize, + ) -> Vec { + self.find_paths_up_to_mode( + source, + source_variant, + target, + target_variant, + ReductionMode::Witness, + limit, + ) + } + + /// Like [`find_all_paths_mode`](Self::find_all_paths_mode) but stops + /// enumeration after collecting `limit` paths. + pub fn find_paths_up_to_mode( + &self, + source: &str, + source_variant: &BTreeMap, + target: &str, + target_variant: &BTreeMap, + mode: ReductionMode, + limit: usize, ) -> Vec { let src = match self.lookup_node(source, source_variant) { Some(idx) => idx, @@ -556,6 +658,7 @@ impl ReductionGraph { paths .iter() + .filter(|p| self.node_path_supports_mode(p, mode)) .map(|p| self.node_path_to_reduction_path(p)) .collect() } @@ -591,6 +694,45 @@ impl ReductionGraph { false } + /// Check if a direct reduction exists by name in a specific mode. + pub fn has_direct_reduction_by_name_mode( + &self, + src: &str, + dst: &str, + mode: ReductionMode, + ) -> bool { + let src_nodes = match self.name_to_nodes.get(src) { + Some(nodes) => nodes, + None => return false, + }; + let dst_nodes = match self.name_to_nodes.get(dst) { + Some(nodes) => nodes, + None => return false, + }; + + let dst_set: HashSet = dst_nodes.iter().copied().collect(); + + for &src_idx in src_nodes { + for edge_ref in self.graph.edges(src_idx) { + if dst_set.contains(&edge_ref.target()) + && Self::edge_supports_mode(edge_ref.weight(), mode) + { + return true; + } + } + } + + false + } + + /// Check if a direct reduction exists from S to T in a specific mode. + pub fn has_direct_reduction_mode( + &self, + mode: ReductionMode, + ) -> bool { + self.has_direct_reduction_by_name_mode(S::NAME, T::NAME, mode) + } + /// Get all registered problem type names (base names). pub fn problem_types(&self) -> Vec<&'static str> { self.name_to_nodes.keys().copied().collect() @@ -729,6 +871,7 @@ impl ReductionGraph { target_name: dst.name, target_variant: dst.variant.clone(), overhead: self.graph[e.id()].overhead.clone(), + capabilities: self.graph[e.id()].capabilities, } }) .collect() @@ -779,6 +922,7 @@ impl ReductionGraph { target_name: dst.name, target_variant: dst.variant.clone(), overhead: self.graph[e.id()].overhead.clone(), + capabilities: self.graph[e.id()].capabilities, } }) .collect() @@ -793,7 +937,7 @@ impl ReductionGraph { name: &str, variant: &BTreeMap, max_hops: usize, - direction: TraversalDirection, + direction: TraversalFlow, ) -> Vec { use std::collections::VecDeque; @@ -812,11 +956,11 @@ impl ReductionGraph { continue; } - let directions: Vec = match direction { - TraversalDirection::Outgoing => vec![petgraph::Direction::Outgoing], - TraversalDirection::Incoming => vec![petgraph::Direction::Incoming], - TraversalDirection::Both => { - vec![petgraph::Direction::Outgoing, petgraph::Direction::Incoming] + let directions = match direction { + TraversalFlow::Outgoing => vec![petgraph::Outgoing], + TraversalFlow::Incoming => vec![petgraph::Incoming], + TraversalFlow::Both => { + vec![petgraph::Outgoing, petgraph::Incoming] } }; @@ -848,7 +992,7 @@ impl ReductionGraph { name: &str, variant: &BTreeMap, max_hops: usize, - direction: TraversalDirection, + direction: TraversalFlow, ) -> Vec { use std::collections::VecDeque; @@ -870,11 +1014,11 @@ impl ReductionGraph { continue; } - let directions: Vec = match direction { - TraversalDirection::Outgoing => vec![petgraph::Direction::Outgoing], - TraversalDirection::Incoming => vec![petgraph::Direction::Incoming], - TraversalDirection::Both => { - vec![petgraph::Direction::Outgoing, petgraph::Direction::Incoming] + let directions = match direction { + TraversalFlow::Outgoing => vec![petgraph::Outgoing], + TraversalFlow::Incoming => vec![petgraph::Incoming], + TraversalFlow::Both => { + vec![petgraph::Outgoing, petgraph::Incoming] } }; @@ -990,6 +1134,7 @@ impl ReductionGraph { let src_node_id = self.graph[edge_ref.source()]; let dst_node_id = self.graph[edge_ref.target()]; let overhead = &edge_ref.weight().overhead; + let capabilities = edge_ref.weight().capabilities; let overhead_fields = overhead .output_size @@ -1013,6 +1158,8 @@ impl ReductionGraph { target: old_to_new[&dst_node_id], overhead: overhead_fields, doc_path, + witness: capabilities.witness, + aggregate: capabilities.aggregate, }); } @@ -1184,7 +1331,78 @@ impl ReductionChain { } } +/// A composed aggregate reduction chain produced by +/// [`ReductionGraph::reduce_aggregate_along_path`]. +pub struct AggregateReductionChain { + steps: Vec>, +} + +impl AggregateReductionChain { + /// Get the final target problem as a type-erased reference. + pub fn target_problem_any(&self) -> &dyn Any { + self.steps + .last() + .expect("AggregateReductionChain has no steps") + .target_problem_any() + } + + /// Get a typed reference to the final target problem. + /// + /// Panics if the actual target type does not match `T`. + pub fn target_problem(&self) -> &T { + self.target_problem_any() + .downcast_ref::() + .expect("AggregateReductionChain target type mismatch") + } + + /// Extract an aggregate value from target space back to source space. + pub fn extract_value_dyn(&self, target_value: serde_json::Value) -> serde_json::Value { + self.steps + .iter() + .rev() + .fold(target_value, |value, step| step.extract_value_dyn(value)) + } +} + +struct WitnessBackedIdentityAggregateStep { + inner: Box, +} + +impl DynAggregateReductionResult for WitnessBackedIdentityAggregateStep { + fn target_problem_any(&self) -> &dyn Any { + self.inner.target_problem_any() + } + + fn extract_value_dyn(&self, target_value: serde_json::Value) -> serde_json::Value { + target_value + } +} + impl ReductionGraph { + fn execute_aggregate_edge( + &self, + edge_idx: EdgeIndex, + input: &dyn Any, + ) -> Option> { + let edge = &self.graph[edge_idx]; + if !Self::edge_supports_mode(edge, ReductionMode::Aggregate) { + return None; + } + + if let Some(edge_fn) = edge.reduce_aggregate_fn { + return Some(edge_fn(input)); + } + + if edge.capabilities.witness && edge.capabilities.aggregate { + let edge_fn = edge.reduce_fn?; + return Some(Box::new(WitnessBackedIdentityAggregateStep { + inner: edge_fn(input), + })); + } + + None + } + /// Execute a reduction path on a source problem instance. /// /// Looks up each edge's `reduce_fn`, chains them, and returns the @@ -1212,7 +1430,10 @@ impl ReductionGraph { let src = self.lookup_node(&window[0].name, &window[0].variant)?; let dst = self.lookup_node(&window[1].name, &window[1].variant)?; let edge_idx = self.graph.find_edge(src, dst)?; - edge_fns.push(self.graph[edge_idx].reduce_fn); + if !Self::edge_supports_mode(&self.graph[edge_idx], ReductionMode::Witness) { + return None; + } + edge_fns.push(self.graph[edge_idx].reduce_fn?); } // Execute the chain let mut steps: Vec> = Vec::new(); @@ -1227,6 +1448,37 @@ impl ReductionGraph { } Some(ReductionChain { steps }) } + + /// Execute an aggregate-value reduction path on a source problem instance. + pub fn reduce_aggregate_along_path( + &self, + path: &ReductionPath, + source: &dyn Any, + ) -> Option { + if path.steps.len() < 2 { + return None; + } + + let mut edge_indices = Vec::new(); + for window in path.steps.windows(2) { + let src = self.lookup_node(&window[0].name, &window[0].variant)?; + let dst = self.lookup_node(&window[1].name, &window[1].variant)?; + let edge_idx = self.graph.find_edge(src, dst)?; + edge_indices.push(edge_idx); + } + + let mut steps: Vec> = Vec::new(); + let step = self.execute_aggregate_edge(edge_indices[0], source)?; + steps.push(step); + for &edge_idx in &edge_indices[1..] { + let step = { + let prev_target = steps.last().unwrap().target_problem_any(); + self.execute_aggregate_edge(edge_idx, prev_target)? + }; + steps.push(step); + } + Some(AggregateReductionChain { steps }) + } } #[cfg(test)] diff --git a/src/rules/mod.rs b/src/rules/mod.rs index c3971e0f7..a7a0c3dde 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -4,7 +4,7 @@ pub mod analysis; pub mod cost; pub mod registry; pub use cost::{CustomCost, Minimize, MinimizeSteps, PathCostFn}; -pub use registry::{ReductionEntry, ReductionOverhead}; +pub use registry::{EdgeCapabilities, ReductionEntry, ReductionOverhead}; pub(crate) mod circuit_spinglass; mod closestvectorproblem_qubo; @@ -97,10 +97,12 @@ pub(crate) mod steinertree_ilp; pub(crate) mod travelingsalesman_ilp; pub use graph::{ - NeighborInfo, NeighborTree, ReductionChain, ReductionEdgeInfo, ReductionGraph, ReductionPath, - ReductionStep, TraversalDirection, + AggregateReductionChain, NeighborInfo, NeighborTree, ReductionChain, ReductionEdgeInfo, + ReductionGraph, ReductionMode, ReductionPath, ReductionStep, TraversalFlow, +}; +pub use traits::{ + AggregateReductionResult, ReduceTo, ReduceToAggregate, ReductionAutoCast, ReductionResult, }; -pub use traits::{ReduceTo, ReductionAutoCast, ReductionResult}; #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { diff --git a/src/rules/registry.rs b/src/rules/registry.rs index a61dd7852..00bd892a7 100644 --- a/src/rules/registry.rs +++ b/src/rules/registry.rs @@ -1,7 +1,7 @@ //! Automatic reduction registration via inventory. use crate::expr::Expr; -use crate::rules::traits::DynReductionResult; +use crate::rules::traits::{DynAggregateReductionResult, DynReductionResult}; use crate::types::ProblemSize; use std::any::Any; use std::collections::HashSet; @@ -83,6 +83,50 @@ impl ReductionOverhead { } } +/// Witness/config reduction executor stored in the inventory. +pub type ReduceFn = fn(&dyn Any) -> Box; + +/// Aggregate/value reduction executor stored in the inventory. +pub type AggregateReduceFn = fn(&dyn Any) -> Box; + +/// Execution capabilities carried by a reduction edge. +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct EdgeCapabilities { + pub witness: bool, + pub aggregate: bool, +} + +impl EdgeCapabilities { + pub const fn witness_only() -> Self { + Self { + witness: true, + aggregate: false, + } + } + + pub const fn aggregate_only() -> Self { + Self { + witness: false, + aggregate: true, + } + } + + pub const fn both() -> Self { + Self { + witness: true, + aggregate: true, + } + } +} + +/// Defaults to `witness_only()` — the conservative choice for edges registered +/// via `#[reduction]`, which are witness/config reductions. +impl Default for EdgeCapabilities { + fn default() -> Self { + Self::witness_only() + } +} + /// A registered reduction entry for static inventory registration. /// Uses function pointers to lazily derive variant fields from `Problem::variant()`. pub struct ReductionEntry { @@ -101,7 +145,14 @@ pub struct ReductionEntry { /// Type-erased reduction executor. /// Takes a `&dyn Any` (must be `&SourceType`), calls `ReduceTo::reduce_to()`, /// and returns the result as a boxed `DynReductionResult`. - pub reduce_fn: fn(&dyn Any) -> Box, + pub reduce_fn: Option, + /// Type-erased aggregate reduction executor. + /// Takes a `&dyn Any` (must be `&SourceType`), calls + /// `ReduceToAggregate::reduce_to_aggregate()`, and returns the result as a + /// boxed `DynAggregateReductionResult`. + pub reduce_aggregate_fn: Option, + /// Capability metadata for runtime path filtering. + pub capabilities: EdgeCapabilities, /// Compiled overhead evaluation function. /// Takes a `&dyn Any` (must be `&SourceType`), calls getter methods directly, /// and returns the computed target problem size. @@ -151,6 +202,7 @@ impl std::fmt::Debug for ReductionEntry { .field("target_variant", &self.target_variant()) .field("overhead", &self.overhead()) .field("module_path", &self.module_path) + .field("capabilities", &self.capabilities) .finish() } } diff --git a/src/rules/test_helpers.rs b/src/rules/test_helpers.rs index a03655075..6053ad187 100644 --- a/src/rules/test_helpers.rs +++ b/src/rules/test_helpers.rs @@ -1,6 +1,7 @@ use crate::rules::{ReductionChain, ReductionResult}; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem, SatisfactionProblem}; +use crate::solvers::BruteForce; +use crate::traits::Problem; +use crate::types::Aggregate; use std::collections::HashSet; fn verify_optimization_round_trip( @@ -10,9 +11,8 @@ fn verify_optimization_round_trip( target_solution_kind: &str, context: &str, ) where - Source: OptimizationProblem + 'static, - ::Value: std::fmt::Debug + PartialEq, - ::Metric: std::fmt::Debug + PartialEq, + Source: Problem + 'static, + ::Value: Aggregate + std::fmt::Debug + PartialEq, Extract: Fn(&[usize]) -> Vec, { assert!( @@ -22,7 +22,7 @@ fn verify_optimization_round_trip( let solver = BruteForce::new(); let reference_solutions: HashSet> = - solver.find_all_best(source).into_iter().collect(); + solver.find_all_witnesses(source).into_iter().collect(); assert!( !reference_solutions.is_empty(), "{context}: direct source solver found no optimal solutions" @@ -48,11 +48,6 @@ fn verify_optimization_round_trip( ); for source_solution in &extracted { let extracted_metric = source.evaluate(source_solution); - assert!( - extracted_metric.is_valid(), - "{context}: extracted source solution is infeasible: {:?}", - source_solution - ); assert_eq!( extracted_metric, reference_metric, "{context}: extracted source objective does not match direct solve" @@ -67,7 +62,8 @@ fn verify_satisfaction_round_trip( target_solution_kind: &str, context: &str, ) where - Source: SatisfactionProblem + 'static, + Source: Problem + 'static, + ::Value: Aggregate + std::fmt::Debug, Extract: Fn(&[usize]) -> Vec, { assert!( @@ -82,9 +78,11 @@ fn verify_satisfaction_round_trip( !extracted.is_empty(), "{context}: no extracted source solutions" ); + let total = ::solve(&BruteForce::new(), source); for source_solution in &extracted { + let value = source.evaluate(source_solution); assert!( - source.evaluate(source_solution), + ::contributes_to_witnesses(&value, &total), "{context}: extracted source solution is not satisfying: {:?}", source_solution ); @@ -97,12 +95,12 @@ pub(crate) fn assert_optimization_round_trip_from_optimization_target( context: &str, ) where R: ReductionResult, - R::Source: OptimizationProblem + 'static, - R::Target: OptimizationProblem + 'static, - ::Value: std::fmt::Debug + PartialEq, - ::Metric: std::fmt::Debug + PartialEq, + R::Source: Problem + 'static, + R::Target: Problem + 'static, + ::Value: Aggregate + std::fmt::Debug + PartialEq, + ::Value: Aggregate, { - let target_solutions = BruteForce::new().find_all_best(reduction.target_problem()); + let target_solutions = BruteForce::new().find_all_witnesses(reduction.target_problem()); verify_optimization_round_trip( source, target_solutions, @@ -118,12 +116,12 @@ pub(crate) fn assert_optimization_round_trip_from_satisfaction_target( context: &str, ) where R: ReductionResult, - R::Source: OptimizationProblem + 'static, - R::Target: SatisfactionProblem + 'static, - ::Value: std::fmt::Debug + PartialEq, - ::Metric: std::fmt::Debug + PartialEq, + R::Source: Problem + 'static, + R::Target: Problem + 'static, + ::Value: Aggregate + std::fmt::Debug + PartialEq, + ::Value: Aggregate, { - let target_solutions = BruteForce::new().find_all_satisfying(reduction.target_problem()); + let target_solutions = BruteForce::new().find_all_witnesses(reduction.target_problem()); verify_optimization_round_trip( source, target_solutions, @@ -138,12 +136,12 @@ pub(crate) fn assert_optimization_round_trip_chain( chain: &ReductionChain, context: &str, ) where - Source: OptimizationProblem + 'static, - Target: OptimizationProblem + 'static, - ::Value: std::fmt::Debug + PartialEq, - ::Metric: std::fmt::Debug + PartialEq, + Source: Problem + 'static, + Target: Problem + 'static, + ::Value: Aggregate + std::fmt::Debug + PartialEq, + ::Value: Aggregate, { - let target_solutions = BruteForce::new().find_all_best(chain.target_problem::()); + let target_solutions = BruteForce::new().find_all_witnesses(chain.target_problem::()); verify_optimization_round_trip( source, target_solutions, @@ -159,10 +157,12 @@ pub(crate) fn assert_satisfaction_round_trip_from_optimization_target( context: &str, ) where R: ReductionResult, - R::Source: SatisfactionProblem + 'static, - R::Target: OptimizationProblem + 'static, + R::Source: Problem + 'static, + R::Target: Problem + 'static, + ::Value: Aggregate + std::fmt::Debug, + ::Value: Aggregate, { - let target_solutions = BruteForce::new().find_all_best(reduction.target_problem()); + let target_solutions = BruteForce::new().find_all_witnesses(reduction.target_problem()); verify_satisfaction_round_trip( source, target_solutions, @@ -178,10 +178,12 @@ pub(crate) fn assert_satisfaction_round_trip_from_satisfaction_target( context: &str, ) where R: ReductionResult, - R::Source: SatisfactionProblem + 'static, - R::Target: SatisfactionProblem + 'static, + R::Source: Problem + 'static, + R::Target: Problem + 'static, + ::Value: Aggregate + std::fmt::Debug, + ::Value: Aggregate, { - let target_solutions = BruteForce::new().find_all_satisfying(reduction.target_problem()); + let target_solutions = BruteForce::new().find_all_witnesses(reduction.target_problem()); verify_satisfaction_round_trip( source, target_solutions, @@ -193,16 +195,18 @@ pub(crate) fn assert_satisfaction_round_trip_from_satisfaction_target( pub(crate) fn solve_optimization_problem

(problem: &P) -> Option> where - P: OptimizationProblem + 'static, + P: Problem + 'static, + P::Value: Aggregate, { - BruteForce::new().find_best(problem) + BruteForce::new().find_witness(problem) } pub(crate) fn solve_satisfaction_problem

(problem: &P) -> Option> where - P: SatisfactionProblem + 'static, + P: Problem + 'static, + P::Value: Aggregate, { - BruteForce::new().find_satisfying(problem) + BruteForce::new().find_witness(problem) } #[cfg(test)] @@ -214,24 +218,24 @@ mod tests { assert_satisfaction_round_trip_from_satisfaction_target, }; use crate::rules::ReductionResult; - use crate::traits::{OptimizationProblem, Problem, SatisfactionProblem}; - use crate::types::{Direction, SolutionSize}; + use crate::traits::Problem; + use crate::types::{Max, Or}; #[derive(Clone)] - struct ToyOptimizationProblem; + struct ToyExtremumProblem; - impl Problem for ToyOptimizationProblem { - const NAME: &'static str = "ToyOptimizationProblem"; - type Metric = SolutionSize; + impl Problem for ToyExtremumProblem { + const NAME: &'static str = "ToyExtremumProblem"; + type Value = Max; fn dims(&self) -> Vec { vec![2, 2] } - fn evaluate(&self, config: &[usize]) -> Self::Metric { + fn evaluate(&self, config: &[usize]) -> Self::Value { match config { - [1, 0] | [0, 1] => SolutionSize::Valid(1), - _ => SolutionSize::Invalid, + [1, 0] | [0, 1] => Max(Some(1)), + _ => Max(None), } } @@ -240,27 +244,19 @@ mod tests { } } - impl OptimizationProblem for ToyOptimizationProblem { - type Value = i32; - - fn direction(&self) -> Direction { - Direction::Maximize - } - } - #[derive(Clone)] - struct ToySatisfactionProblem; + struct ToyOrProblem; - impl Problem for ToySatisfactionProblem { - const NAME: &'static str = "ToySatisfactionProblem"; - type Metric = bool; + impl Problem for ToyOrProblem { + const NAME: &'static str = "ToyOrProblem"; + type Value = Or; fn dims(&self) -> Vec { vec![2, 2] } - fn evaluate(&self, config: &[usize]) -> Self::Metric { - matches!(config, [1, 0] | [0, 1]) + fn evaluate(&self, config: &[usize]) -> Self::Value { + Or(matches!(config, [1, 0] | [0, 1])) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -268,15 +264,13 @@ mod tests { } } - impl SatisfactionProblem for ToySatisfactionProblem {} - struct OptToOptReduction { - target: ToyOptimizationProblem, + target: ToyExtremumProblem, } impl ReductionResult for OptToOptReduction { - type Source = ToyOptimizationProblem; - type Target = ToyOptimizationProblem; + type Source = ToyExtremumProblem; + type Target = ToyExtremumProblem; fn target_problem(&self) -> &Self::Target { &self.target @@ -288,12 +282,12 @@ mod tests { } struct OptToSatReduction { - target: ToySatisfactionProblem, + target: ToyOrProblem, } impl ReductionResult for OptToSatReduction { - type Source = ToyOptimizationProblem; - type Target = ToySatisfactionProblem; + type Source = ToyExtremumProblem; + type Target = ToyOrProblem; fn target_problem(&self) -> &Self::Target { &self.target @@ -305,12 +299,12 @@ mod tests { } struct SatToOptReduction { - target: ToyOptimizationProblem, + target: ToyExtremumProblem, } impl ReductionResult for SatToOptReduction { - type Source = ToySatisfactionProblem; - type Target = ToyOptimizationProblem; + type Source = ToyOrProblem; + type Target = ToyExtremumProblem; fn target_problem(&self) -> &Self::Target { &self.target @@ -322,12 +316,12 @@ mod tests { } struct SatToSatReduction { - target: ToySatisfactionProblem, + target: ToyOrProblem, } impl ReductionResult for SatToSatReduction { - type Source = ToySatisfactionProblem; - type Target = ToySatisfactionProblem; + type Source = ToyOrProblem; + type Target = ToyOrProblem; fn target_problem(&self) -> &Self::Target { &self.target @@ -340,41 +334,41 @@ mod tests { #[test] fn test_optimization_round_trip_wrappers_accept_identity_reductions() { - let source = ToyOptimizationProblem; + let source = ToyExtremumProblem; assert_optimization_round_trip_from_optimization_target( &source, &OptToOptReduction { - target: ToyOptimizationProblem, + target: ToyExtremumProblem, }, - "opt->opt", + "extremum->extremum", ); assert_optimization_round_trip_from_satisfaction_target( &source, &OptToSatReduction { - target: ToySatisfactionProblem, + target: ToyOrProblem, }, - "opt->sat", + "extremum->witness", ); } #[test] fn test_satisfaction_round_trip_wrappers_accept_identity_reductions() { - let source = ToySatisfactionProblem; + let source = ToyOrProblem; assert_satisfaction_round_trip_from_optimization_target( &source, &SatToOptReduction { - target: ToyOptimizationProblem, + target: ToyExtremumProblem, }, - "sat->opt", + "witness->extremum", ); assert_satisfaction_round_trip_from_satisfaction_target( &source, &SatToSatReduction { - target: ToySatisfactionProblem, + target: ToyOrProblem, }, - "sat->sat", + "witness->witness", ); } } diff --git a/src/rules/traits.rs b/src/rules/traits.rs index c4b672353..a46dc19ec 100644 --- a/src/rules/traits.rs +++ b/src/rules/traits.rs @@ -1,6 +1,8 @@ //! Core traits for problem reductions. use crate::traits::Problem; +use serde::de::DeserializeOwned; +use serde::Serialize; use std::any::Any; use std::marker::PhantomData; @@ -49,7 +51,7 @@ pub trait ReductionResult { /// /// // Solve and extract solutions /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(is_problem); +/// let solutions = solver.find_all_witnesses(is_problem); /// let sat_solutions: Vec<_> = solutions.iter() /// .map(|s| reduction.extract_solution(s)) /// .collect(); @@ -62,6 +64,36 @@ pub trait ReduceTo: Problem { fn reduce_to(&self) -> Self::Result; } +/// Result of reducing a source problem to a target problem for aggregate values. +/// +/// Unlike [`ReductionResult`], this trait maps aggregate values back from target +/// space to source space instead of mapping witness configurations. +pub trait AggregateReductionResult { + /// The source problem type. + type Source: Problem; + /// The target problem type. + type Target: Problem; + + /// Get a reference to the target problem. + fn target_problem(&self) -> &Self::Target; + + /// Extract an aggregate value from target problem space back to source space. + fn extract_value( + &self, + target_value: ::Value, + ) -> ::Value; +} + +/// Trait for problems that can be reduced to target type T for aggregate-value +/// workflows. +pub trait ReduceToAggregate: Problem { + /// The reduction result type. + type Result: AggregateReductionResult; + + /// Reduce this problem to the target problem type. + fn reduce_to_aggregate(&self) -> Self::Result; +} + /// Generic reduction result for natural-edge (subtype) reductions. /// /// Used when a problem on a specific graph type is trivially reducible to @@ -97,6 +129,21 @@ impl ReductionResult for ReductionAutoCast { } } +impl> AggregateReductionResult + for ReductionAutoCast +{ + type Source = S; + type Target = T; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, target_value: T::Value) -> S::Value { + target_value + } +} + /// Type-erased reduction result for runtime-discovered paths. /// /// Implemented automatically for all `ReductionResult` types via blanket impl. @@ -120,6 +167,33 @@ where } } +/// Type-erased aggregate reduction result for runtime-discovered paths. +pub trait DynAggregateReductionResult { + /// Get the target problem as a type-erased reference. + fn target_problem_any(&self) -> &dyn Any; + /// Extract an aggregate value from target space to source space. + fn extract_value_dyn(&self, target_value: serde_json::Value) -> serde_json::Value; +} + +impl DynAggregateReductionResult for R +where + R::Target: 'static, + ::Value: Serialize + DeserializeOwned, + ::Value: Serialize, +{ + fn target_problem_any(&self) -> &dyn Any { + self.target_problem() as &dyn Any + } + + fn extract_value_dyn(&self, target_value: serde_json::Value) -> serde_json::Value { + let target_value = serde_json::from_value(target_value) + .expect("DynAggregateReductionResult target value deserialize failed"); + let source_value = self.extract_value(target_value); + serde_json::to_value(source_value) + .expect("DynAggregateReductionResult source value serialize failed") + } +} + #[cfg(test)] #[path = "../unit_tests/rules/traits.rs"] mod tests; diff --git a/src/solvers/brute_force.rs b/src/solvers/brute_force.rs index e84b7190e..caf8ca817 100644 --- a/src/solvers/brute_force.rs +++ b/src/solvers/brute_force.rs @@ -2,12 +2,14 @@ use crate::config::DimsIterator; use crate::solvers::Solver; -use crate::traits::{OptimizationProblem, Problem}; +use crate::traits::Problem; +use crate::types::Aggregate; /// A brute force solver that enumerates all possible configurations. /// /// This solver is exponential in the number of variables but guarantees -/// finding all optimal solutions. +/// finding the full aggregate value and all witness configurations when the +/// aggregate type supports witnesses. #[derive(Debug, Clone, Default)] pub struct BruteForce; @@ -17,68 +19,67 @@ impl BruteForce { Self } - /// Find all optimal solutions for an optimization problem. - /// - /// Returns all configurations that achieve the optimal metric value. - /// Returns empty vec if all configurations are invalid. - pub fn find_all_best(&self, problem: &P) -> Vec> { - let iter = DimsIterator::new(problem.dims()); - let direction = problem.direction(); - let mut best_solutions: Vec> = vec![]; - let mut best_metric: Option> = None; - - for config in iter { - let metric = problem.evaluate(&config); + /// Find one witness configuration when the aggregate value admits them. + pub fn find_witness

(&self, problem: &P) -> Option> + where + P: Problem, + P::Value: Aggregate, + { + self.find_all_witnesses(problem).into_iter().next() + } - // Skip infeasible solutions - if !metric.is_valid() { - continue; - } + /// Find all witness configurations for witness-supporting aggregates. + pub fn find_all_witnesses

(&self, problem: &P) -> Vec> + where + P: Problem, + P::Value: Aggregate, + { + let total = self.solve(problem); - let dominated = match &best_metric { - None => false, - Some(current_best) => current_best.is_better(&metric, direction), - }; + if !P::Value::supports_witnesses() { + return vec![]; + } - if dominated { - continue; - } + DimsIterator::new(problem.dims()) + .filter(|config| { + let value = problem.evaluate(config); + P::Value::contributes_to_witnesses(&value, &total) + }) + .collect() + } - let dominates = match &best_metric { - None => true, - Some(current_best) => metric.is_better(current_best, direction), - }; + /// Solve a problem and collect all witness configurations in one passable API. + pub fn solve_with_witnesses

(&self, problem: &P) -> (P::Value, Vec>) + where + P: Problem, + P::Value: Aggregate, + { + let total = self.solve(problem); - if dominates { - best_metric = Some(metric); - best_solutions.clear(); - best_solutions.push(config); - } else if best_metric.is_some() { - // Equal quality - add to solutions - best_solutions.push(config); - } + if !P::Value::supports_witnesses() { + return (total, vec![]); } - best_solutions - } + let witnesses = DimsIterator::new(problem.dims()) + .filter(|config| { + let value = problem.evaluate(config); + P::Value::contributes_to_witnesses(&value, &total) + }) + .collect(); - /// Find all satisfying solutions for constraint satisfaction problems. - /// - /// Returns all configurations where `problem.evaluate(config)` returns `true`. - pub fn find_all_satisfying>(&self, problem: &P) -> Vec> { - DimsIterator::new(problem.dims()) - .filter(|config| problem.evaluate(config)) - .collect() + (total, witnesses) } } impl Solver for BruteForce { - fn find_best(&self, problem: &P) -> Option> { - self.find_all_best(problem).into_iter().next() - } - - fn find_satisfying>(&self, problem: &P) -> Option> { - DimsIterator::new(problem.dims()).find(|config| problem.evaluate(config)) + fn solve

(&self, problem: &P) -> P::Value + where + P: Problem, + P::Value: Aggregate, + { + DimsIterator::new(problem.dims()) + .map(|config| problem.evaluate(&config)) + .fold(P::Value::identity(), P::Value::combine) } } diff --git a/src/solvers/ilp/mod.rs b/src/solvers/ilp/mod.rs index f23f70ff2..b09109814 100644 --- a/src/solvers/ilp/mod.rs +++ b/src/solvers/ilp/mod.rs @@ -24,3 +24,4 @@ mod solver; pub use solver::ILPSolver; +pub use solver::SolveViaReductionError; diff --git a/src/solvers/ilp/solver.rs b/src/solvers/ilp/solver.rs index a2ba9b985..0f12d5562 100644 --- a/src/solvers/ilp/solver.rs +++ b/src/solvers/ilp/solver.rs @@ -2,7 +2,7 @@ use crate::models::algebraic::{Comparison, ObjectiveSense, VariableDomain, ILP}; use crate::models::misc::TimetableDesign; -use crate::rules::{ReduceTo, ReductionResult}; +use crate::rules::{ReduceTo, ReductionMode, ReductionResult}; #[cfg(not(feature = "ilp-highs"))] use good_lp::default_solver; #[cfg(feature = "ilp-highs")] @@ -40,6 +40,33 @@ pub struct ILPSolver { pub time_limit: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SolveViaReductionError { + WitnessPathRequired { name: String }, + NoReductionPath { name: String }, + NoSolution { name: String }, +} + +impl std::fmt::Display for SolveViaReductionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SolveViaReductionError::WitnessPathRequired { name } => write!( + f, + "ILP solving requires a witness-capable source problem and reduction path; only aggregate-value solving is available for {}.", + name + ), + SolveViaReductionError::NoReductionPath { name } => { + write!(f, "No reduction path from {} to ILP", name) + } + SolveViaReductionError::NoSolution { name } => { + write!(f, "ILP solver found no solution for {}", name) + } + } + } +} + +impl std::error::Error for SolveViaReductionError {} + impl ILPSolver { /// Create a new ILP solver with default settings. pub fn new() -> Self { @@ -188,36 +215,33 @@ impl ILPSolver { None } - /// Solve a type-erased problem by finding a reduction path to ILP. - /// - /// Tries all ILP variants, picks the cheapest path, reduces, solves, - /// and extracts the solution back. Falls back to direct ILP solve if - /// the problem is already an ILP type. - /// - /// Returns `None` if no path to ILP exists or the solver finds no solution. - pub fn solve_via_reduction( + fn supports_direct_dyn(&self, any: &dyn std::any::Any) -> bool { + any.is::>() || any.is::>() || any.is::() + } + + fn best_path_to_ilp( &self, + graph: &crate::rules::ReductionGraph, name: &str, variant: &std::collections::BTreeMap, - instance: &dyn std::any::Any, - ) -> Option> { - // Direct ILP solve if the problem is already ILP - if let Some(config) = self.solve_dyn(instance) { - return Some(config); - } - - use crate::rules::{MinimizeSteps, ReductionGraph}; + mode: ReductionMode, + ) -> Option { use crate::types::ProblemSize; - let graph = ReductionGraph::new(); let ilp_variants = graph.variants_for("ILP"); let input_size = ProblemSize::new(vec![]); - let mut best_path = None; + for dv in &ilp_variants { - if let Some(path) = - graph.find_cheapest_path(name, variant, "ILP", dv, &input_size, &MinimizeSteps) - { + if let Some(path) = graph.find_cheapest_path_mode( + name, + variant, + "ILP", + dv, + mode, + &input_size, + &crate::rules::MinimizeSteps, + ) { let is_better = best_path .as_ref() .is_none_or(|current: &crate::rules::ReductionPath| path.len() < current.len()); @@ -227,10 +251,68 @@ impl ILPSolver { } } - let path = best_path?; - let chain = graph.reduce_along_path(&path, instance)?; - let ilp_solution = self.solve_dyn(chain.target_problem_any())?; - Some(chain.extract_solution(&ilp_solution)) + best_path + } + + pub fn try_solve_via_reduction( + &self, + name: &str, + variant: &std::collections::BTreeMap, + instance: &dyn std::any::Any, + ) -> Result, SolveViaReductionError> { + if self.supports_direct_dyn(instance) { + return self + .solve_dyn(instance) + .ok_or_else(|| SolveViaReductionError::NoSolution { + name: name.to_string(), + }); + } + + let graph = crate::rules::ReductionGraph::new(); + + let Some(path) = self.best_path_to_ilp(&graph, name, variant, ReductionMode::Witness) + else { + if self + .best_path_to_ilp(&graph, name, variant, ReductionMode::Aggregate) + .is_some() + { + return Err(SolveViaReductionError::WitnessPathRequired { + name: name.to_string(), + }); + } + + return Err(SolveViaReductionError::NoReductionPath { + name: name.to_string(), + }); + }; + + let chain = graph.reduce_along_path(&path, instance).ok_or_else(|| { + SolveViaReductionError::WitnessPathRequired { + name: name.to_string(), + } + })?; + let ilp_solution = self.solve_dyn(chain.target_problem_any()).ok_or_else(|| { + SolveViaReductionError::NoSolution { + name: name.to_string(), + } + })?; + Ok(chain.extract_solution(&ilp_solution)) + } + + /// Solve a type-erased problem by finding a reduction path to ILP. + /// + /// Tries all ILP variants, picks the cheapest path, reduces, solves, + /// and extracts the solution back. Falls back to direct ILP solve if + /// the problem is already an ILP type. + /// + /// Returns `None` if no path to ILP exists or the solver finds no solution. + pub fn solve_via_reduction( + &self, + name: &str, + variant: &std::collections::BTreeMap, + instance: &dyn std::any::Any, + ) -> Option> { + self.try_solve_via_reduction(name, variant, instance).ok() } } diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index 894bc8207..074841c31 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -10,16 +10,13 @@ pub use brute_force::BruteForce; #[cfg(feature = "ilp-solver")] pub use ilp::ILPSolver; -use crate::traits::{OptimizationProblem, Problem}; +use crate::traits::Problem; /// Trait for problem solvers. pub trait Solver { - /// Find one optimal solution for an optimization problem. - /// - /// Returns a single configuration that achieves the optimal metric value, - /// or `None` if no feasible configuration exists. - fn find_best(&self, problem: &P) -> Option>; - - /// Find any satisfying solution for a satisfaction problem (Metric = bool). - fn find_satisfying>(&self, problem: &P) -> Option>; + /// Solve a problem to its aggregate value. + fn solve

(&self, problem: &P) -> P::Value + where + P: Problem, + P::Value: crate::types::Aggregate; } diff --git a/src/topology/directed_graph.rs b/src/topology/directed_graph.rs index d8491206a..84fe30027 100644 --- a/src/topology/directed_graph.rs +++ b/src/topology/directed_graph.rs @@ -115,7 +115,7 @@ impl DirectedGraph { /// These are all vertices `u` such that there is an arc `u → v`. pub fn predecessors(&self, v: usize) -> Vec { self.inner - .neighbors_directed(NodeIndex::new(v), petgraph::Direction::Incoming) + .neighbors_directed(NodeIndex::new(v), petgraph::Incoming) .map(|n| n.index()) .collect() } diff --git a/src/traits.rs b/src/traits.rs index 3ae298392..5792e2407 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,18 +1,18 @@ //! Core traits for problem definitions. -/// Minimal problem trait — a problem is a function from configuration to metric. +/// Minimal problem trait — a problem is a function from configuration to value. /// /// This trait defines the interface for computational problems that can be /// solved by enumeration or reduction to other problems. pub trait Problem: Clone { /// Base name of this problem type (e.g., "MaximumIndependentSet"). const NAME: &'static str; - /// The evaluation metric type. - type Metric: Clone; + /// The evaluation value type. + type Value: Clone; /// Configuration space dimensions. Each entry is the cardinality of that variable. fn dims(&self) -> Vec; /// Evaluate the problem on a configuration. - fn evaluate(&self, config: &[usize]) -> Self::Metric; + fn evaluate(&self, config: &[usize]) -> Self::Value; /// Number of variables (derived from dims). fn num_variables(&self) -> usize { self.dims().len() @@ -33,24 +33,6 @@ pub trait Problem: Clone { } } -/// Extension for problems with a numeric objective to optimize. -/// -/// The supertrait bound guarantees `Metric = SolutionSize`, -/// so the solver can call `metric.is_valid()` and `metric.is_better()` -/// directly — no per-problem customization needed. -pub trait OptimizationProblem: Problem> { - /// The inner objective value type (e.g., `i32`, `f64`). - type Value: PartialOrd + Clone; - /// Whether to maximize or minimize the metric. - fn direction(&self) -> crate::types::Direction; -} - -/// Marker trait for satisfaction (decision) problems. -/// -/// Satisfaction problems evaluate configurations to `bool`: -/// `true` if the configuration satisfies all constraints, `false` otherwise. -pub trait SatisfactionProblem: Problem {} - /// Marker trait for explicitly declared problem variants. /// /// Implemented automatically by [`declare_variants!`] for each concrete type. diff --git a/src/types.rs b/src/types.rs index 2e615d0e9..b9236aec7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,6 +1,6 @@ //! Common types used across the problemreductions library. -use serde::de::{self, Visitor}; +use serde::de::{self, DeserializeOwned, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt; @@ -164,100 +164,345 @@ impl From for One { /// Backward-compatible alias for `One`. pub type Unweighted = One; -/// Result of evaluating a constrained optimization problem. -/// -/// For optimization problems with constraints (like MaximumIndependentSet), -/// configurations may be infeasible. This enum explicitly represents validity. -/// -/// # Example -/// -/// ``` -/// use problemreductions::types::SolutionSize; -/// -/// let valid = SolutionSize::Valid(42); -/// assert!(valid.is_valid()); -/// assert_eq!(valid.size(), Some(&42)); -/// -/// let invalid: SolutionSize = SolutionSize::Invalid; -/// assert!(!invalid.is_valid()); -/// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] -pub enum SolutionSize { - /// A valid (feasible) solution with the given objective value. - Valid(T), - /// An invalid (infeasible) solution that violates constraints. - #[default] - Invalid, -} - -impl SolutionSize { - /// Returns true if this is a valid solution. - pub fn is_valid(&self) -> bool { - matches!(self, SolutionSize::Valid(_)) +/// Foldable aggregate values for enumerating a problem's configuration space. +pub trait Aggregate: Clone + fmt::Debug + Serialize + DeserializeOwned { + /// Neutral element for folding. + fn identity() -> Self; + + /// Associative combine operation. + fn combine(self, other: Self) -> Self; + + /// Whether this aggregate admits representative witness configurations. + fn supports_witnesses() -> bool { + false } - /// Returns the size if valid, None if invalid. - pub fn size(&self) -> Option<&T> { - match self { - SolutionSize::Valid(t) => Some(t), - SolutionSize::Invalid => None, - } + /// Whether a configuration-level value belongs to the witness set + /// for the final aggregate value. + fn contributes_to_witnesses(_config_value: &Self, _total: &Self) -> bool { + false } +} + +/// Maximum aggregate over feasible values. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Max(pub Option); - /// Unwraps the size, panicking if invalid. - pub fn unwrap(self) -> T { - match self { - SolutionSize::Valid(t) => t, - SolutionSize::Invalid => panic!("called unwrap on Invalid SolutionSize"), +impl Aggregate for Max { + fn identity() -> Self { + Max(None) + } + + fn combine(self, other: Self) -> Self { + use std::cmp::Ordering; + + match (self.0, other.0) { + (None, rhs) => Max(rhs), + (lhs, None) => Max(lhs), + (Some(lhs), Some(rhs)) => { + let ord = lhs.partial_cmp(&rhs).expect("cannot compare values (NaN?)"); + match ord { + Ordering::Less => Max(Some(rhs)), + Ordering::Equal | Ordering::Greater => Max(Some(lhs)), + } + } } } - /// Maps the inner value if valid. - pub fn map U>(self, f: F) -> SolutionSize { - match self { - SolutionSize::Valid(t) => SolutionSize::Valid(f(t)), - SolutionSize::Invalid => SolutionSize::Invalid, + fn supports_witnesses() -> bool { + true + } + + fn contributes_to_witnesses(config_value: &Self, total: &Self) -> bool { + matches!((config_value, total), (Max(Some(value)), Max(Some(best))) if value == best) + } +} + +impl fmt::Display for Max { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.0 { + Some(value) => write!(f, "Max({value})"), + None => write!(f, "Max(None)"), } } } -impl SolutionSize { - /// Returns true if self is a better solution than other for the given direction. - /// - /// - For maximization: larger values are better - /// - For minimization: smaller values are better - /// - Valid solutions are always better than invalid ones - /// - Two invalid solutions are equally bad (neither is better) - /// - /// # Panics - /// - /// Panics if comparing two valid values that are not comparable (e.g., NaN for f64). - pub fn is_better(&self, other: &Self, direction: Direction) -> bool { - match (self, other) { - (SolutionSize::Valid(a), SolutionSize::Valid(b)) => { - use std::cmp::Ordering; - let ord = a.partial_cmp(b).expect("cannot compare values (NaN?)"); - match direction { - Direction::Maximize => ord == Ordering::Greater, - Direction::Minimize => ord == Ordering::Less, +impl Max { + pub fn is_valid(&self) -> bool { + self.0.is_some() + } + + pub fn size(&self) -> Option<&V> { + self.0.as_ref() + } + + pub fn unwrap(self) -> V { + self.0.expect("called unwrap on invalid Max value") + } +} + +/// Minimum aggregate over feasible values. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Min(pub Option); + +impl Aggregate for Min { + fn identity() -> Self { + Min(None) + } + + fn combine(self, other: Self) -> Self { + use std::cmp::Ordering; + + match (self.0, other.0) { + (None, rhs) => Min(rhs), + (lhs, None) => Min(lhs), + (Some(lhs), Some(rhs)) => { + let ord = lhs.partial_cmp(&rhs).expect("cannot compare values (NaN?)"); + match ord { + Ordering::Greater => Min(Some(rhs)), + Ordering::Equal | Ordering::Less => Min(Some(lhs)), } } - (SolutionSize::Valid(_), SolutionSize::Invalid) => true, - (SolutionSize::Invalid, SolutionSize::Valid(_)) => false, - (SolutionSize::Invalid, SolutionSize::Invalid) => false, } } + + fn supports_witnesses() -> bool { + true + } + + fn contributes_to_witnesses(config_value: &Self, total: &Self) -> bool { + matches!((config_value, total), (Min(Some(value)), Min(Some(best))) if value == best) + } +} + +impl fmt::Display for Min { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.0 { + Some(value) => write!(f, "Min({value})"), + None => write!(f, "Min(None)"), + } + } +} + +impl Min { + pub fn is_valid(&self) -> bool { + self.0.is_some() + } + + pub fn size(&self) -> Option<&V> { + self.0.as_ref() + } + + pub fn unwrap(self) -> V { + self.0.expect("called unwrap on invalid Min value") + } +} + +/// Sum aggregate for value-only problems. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Sum(pub W); + +impl Aggregate for Sum { + fn identity() -> Self { + Sum(W::zero()) + } + + fn combine(self, other: Self) -> Self { + let mut total = self.0; + total += other.0; + Sum(total) + } } -/// Optimization direction. +impl fmt::Display for Sum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Sum({})", self.0) + } +} + +/// Disjunction aggregate for existential satisfaction. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum Direction { - /// Maximize the objective value. +pub struct Or(pub bool); + +impl Or { + pub fn is_valid(&self) -> bool { + self.0 + } + + pub fn unwrap(self) -> bool { + self.0 + } +} + +impl Aggregate for Or { + fn identity() -> Self { + Or(false) + } + + fn combine(self, other: Self) -> Self { + Or(self.0 || other.0) + } + + fn supports_witnesses() -> bool { + true + } + + fn contributes_to_witnesses(config_value: &Self, total: &Self) -> bool { + config_value.0 && total.0 + } +} + +impl fmt::Display for Or { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Or({})", self.0) + } +} + +impl std::ops::Not for Or { + type Output = bool; + + fn not(self) -> Self::Output { + !self.0 + } +} + +impl PartialEq for Or { + fn eq(&self, other: &bool) -> bool { + self.0 == *other + } +} + +impl PartialEq for bool { + fn eq(&self, other: &Or) -> bool { + *self == other.0 + } +} + +/// Conjunction aggregate for universal satisfaction. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct And(pub bool); + +impl Aggregate for And { + fn identity() -> Self { + And(true) + } + + fn combine(self, other: Self) -> Self { + And(self.0 && other.0) + } +} + +impl fmt::Display for And { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "And({})", self.0) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ExtremumSense { Maximize, - /// Minimize the objective value. Minimize, } +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Extremum { + pub sense: ExtremumSense, + pub value: Option, +} + +impl Extremum { + pub fn maximize(value: Option) -> Self { + Self { + sense: ExtremumSense::Maximize, + value, + } + } + + pub fn minimize(value: Option) -> Self { + Self { + sense: ExtremumSense::Minimize, + value, + } + } + + pub fn is_valid(&self) -> bool { + self.value.is_some() + } + + pub fn size(&self) -> Option<&V> { + self.value.as_ref() + } + + pub fn unwrap(self) -> V { + self.value.expect("called unwrap on invalid Extremum value") + } +} + +impl Aggregate for Extremum { + fn identity() -> Self { + Self::maximize(None) + } + + fn combine(self, other: Self) -> Self { + use std::cmp::Ordering; + + match (self.value, other.value) { + (None, rhs) => Self { + sense: other.sense, + value: rhs, + }, + (lhs, None) => Self { + sense: self.sense, + value: lhs, + }, + (Some(lhs), Some(rhs)) => { + assert_eq!( + self.sense, other.sense, + "cannot combine Extremum values with different senses" + ); + let ord = lhs.partial_cmp(&rhs).expect("cannot compare values (NaN?)"); + let keep_self = match self.sense { + ExtremumSense::Maximize => matches!(ord, Ordering::Equal | Ordering::Greater), + ExtremumSense::Minimize => matches!(ord, Ordering::Equal | Ordering::Less), + }; + if keep_self { + Self { + sense: self.sense, + value: Some(lhs), + } + } else { + Self { + sense: other.sense, + value: Some(rhs), + } + } + } + } + } + + fn supports_witnesses() -> bool { + true + } + + fn contributes_to_witnesses(config_value: &Self, total: &Self) -> bool { + matches!( + (config_value.value.as_ref(), total.value.as_ref()), + (Some(value), Some(best)) if config_value.sense == total.sense && value == best + ) + } +} + +impl fmt::Display for Extremum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match (&self.sense, &self.value) { + (ExtremumSense::Maximize, Some(value)) => write!(f, "Max({value})"), + (ExtremumSense::Maximize, None) => write!(f, "Max(None)"), + (ExtremumSense::Minimize, Some(value)) => write!(f, "Min({value})"), + (ExtremumSense::Minimize, None) => write!(f, "Min(None)"), + } + } +} + /// Problem size metadata (varies by problem type). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ProblemSize { diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index adca8f825..34c34e45e 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -173,7 +173,7 @@ fn test_find_model_example_minimum_dummy_activities_pert() { assert_eq!(example.problem, "MinimumDummyActivitiesPert"); assert_eq!(example.variant, problem.variant); assert!(example.instance.is_object()); - assert_eq!(example.optimal_value, serde_json::json!({"Valid": 2})); + assert_eq!(example.optimal_value, serde_json::json!(2)); assert!( !example.optimal_config.is_empty(), "canonical example should include an optimal merge selection" @@ -468,7 +468,7 @@ fn model_specs_are_optimal() { return None; } let entry = find_variant_entry(name, &variant)?; - let (config, _) = (entry.solve_fn)(spec.instance.as_any())?; + let (config, _) = (entry.solve_witness_fn)(spec.instance.as_any())?; Some(config) }) .unwrap_or_else(|| { @@ -553,28 +553,33 @@ fn rule_specs_solution_pairs_are_consistent() { pair.target_config.len(), target.dims_dyn().len() ); - // Verify configs produce non-Invalid / non-false evaluations + // Verify configs produce feasible witness-capable evaluations. + let source_eval = source.evaluate_dyn(&pair.source_config); + let target_eval = target.evaluate_dyn(&pair.target_config); let source_val = source.evaluate_json(&pair.source_config); - let target_val = target.evaluate_json(&pair.target_config); assert_ne!( - source_val, - serde_json::json!("Invalid"), - "Rule {label}: source_config evaluates to Invalid" + source_eval, "Max(None)", + "Rule {label}: source_config evaluates to Max(None)" ); assert_ne!( - target_val, - serde_json::json!("Invalid"), - "Rule {label}: target_config evaluates to Invalid" + source_eval, "Min(None)", + "Rule {label}: source_config evaluates to Min(None)" ); assert_ne!( - source_val, - serde_json::json!(false), - "Rule {label}: source_config evaluates to false" + source_eval, "Or(false)", + "Rule {label}: source_config evaluates to Or(false)" ); assert_ne!( - target_val, - serde_json::json!(false), - "Rule {label}: target_config evaluates to false" + target_eval, "Max(None)", + "Rule {label}: target_config evaluates to Max(None)" + ); + assert_ne!( + target_eval, "Min(None)", + "Rule {label}: target_config evaluates to Min(None)" + ); + assert_ne!( + target_eval, "Or(false)", + "Rule {label}: target_config evaluates to Or(false)" ); // Round-trip: extract_solution(target_config) must produce a valid // source config with the same evaluation value diff --git a/src/unit_tests/export.rs b/src/unit_tests/export.rs index 27776a8cf..c1c9a578f 100644 --- a/src/unit_tests/export.rs +++ b/src/unit_tests/export.rs @@ -118,7 +118,7 @@ fn test_write_example_db_uses_one_line_per_example_entry() { // Add richer data so the one-line-per-entry format is meaningful db.models[0].instance = serde_json::json!({"n": 5, "edges": [[0, 1], [1, 2]]}); db.models[0].optimal_config = vec![1, 0, 1]; - db.models[0].optimal_value = serde_json::json!({"Valid": 2}); + db.models[0].optimal_value = serde_json::json!(2); db.rules[0].source.instance = serde_json::json!({"n": 3, "edges": [[0, 1], [1, 2]]}); db.rules[0].target.instance = serde_json::json!({"m": 4, "weights": [1, 2, 3, 4]}); db.rules[0].solutions = vec![SolutionPair { @@ -229,11 +229,11 @@ fn model_example_new() { variant_to_map(vec![("graph", "SimpleGraph"), ("weight", "i32")]), serde_json::json!({"num_vertices": 3, "edges": [[0, 1], [1, 2]]}), vec![1, 0, 1], - serde_json::json!({"Valid": 2}), + serde_json::json!(2), ); assert_eq!(example.problem, "MaximumIndependentSet"); assert_eq!(example.optimal_config, vec![1, 0, 1]); - assert_eq!(example.optimal_value, serde_json::json!({"Valid": 2})); + assert_eq!(example.optimal_value, serde_json::json!(2)); assert!(example.instance.is_object()); } @@ -286,7 +286,7 @@ fn write_model_example_to_creates_json_file() { variant: variant_to_map(vec![("graph", "SimpleGraph")]), instance: serde_json::json!({"n": 3}), optimal_config: vec![1, 0, 1], - optimal_value: serde_json::json!({"Valid": 2}), + optimal_value: serde_json::json!(2), }; write_model_example_to(&dir, "test_model", &example); let path = dir.join("test_model.json"); diff --git a/src/unit_tests/graph_models.rs b/src/unit_tests/graph_models.rs index 363d10cc4..95979843f 100644 --- a/src/unit_tests/graph_models.rs +++ b/src/unit_tests/graph_models.rs @@ -9,8 +9,8 @@ use crate::models::graph::minimum_vertex_cover::is_vertex_cover; use crate::models::graph::{KColoring, MaximumIndependentSet, MinimumVertexCover}; use crate::prelude::*; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::{Max, Min}; use crate::variant::{K1, K2, K3, K4}; // ============================================================================= @@ -61,10 +61,10 @@ mod maximum_independent_set { MaximumIndependentSet::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), vec![1i32; 4]); // Valid: select 0 and 2 (not adjacent) - assert_eq!(problem.evaluate(&[1, 0, 1, 0]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[1, 0, 1, 0]), Max(Some(2))); // Valid: select 1 and 3 (not adjacent) - assert_eq!(problem.evaluate(&[0, 1, 0, 1]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[0, 1, 0, 1]), Max(Some(2))); } #[test] @@ -73,10 +73,10 @@ mod maximum_independent_set { MaximumIndependentSet::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), vec![1i32; 4]); // Invalid: 0 and 1 are adjacent - returns Invalid - assert_eq!(problem.evaluate(&[1, 1, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 0, 0]), Max(None)); // Invalid: 2 and 3 are adjacent - assert_eq!(problem.evaluate(&[0, 0, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 1, 1]), Max(None)); } #[test] @@ -84,7 +84,7 @@ mod maximum_independent_set { let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); // Empty selection is valid with size 0 - assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0, 0, 0]), Max(Some(0))); } #[test] @@ -93,10 +93,10 @@ mod maximum_independent_set { MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![10, 20, 30]); // Select vertex 2 (weight 30) - assert_eq!(problem.evaluate(&[0, 0, 1]), SolutionSize::Valid(30)); + assert_eq!(problem.evaluate(&[0, 0, 1]), Max(Some(30))); // Select vertices 0 and 2 (weights 10 + 30 = 40) - assert_eq!(problem.evaluate(&[1, 0, 1]), SolutionSize::Valid(40)); + assert_eq!(problem.evaluate(&[1, 0, 1]), Max(Some(40))); } #[test] @@ -108,7 +108,7 @@ mod maximum_independent_set { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // All solutions should have exactly 1 vertex selected assert_eq!(solutions.len(), 3); // Three equivalent solutions for sol in &solutions { @@ -125,13 +125,13 @@ mod maximum_independent_set { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Maximum size is 2 for sol in &solutions { let size: usize = sol.iter().sum(); assert_eq!(size, 2); // Verify it's valid (evaluate returns Valid, not Invalid) - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(sol), Max(Some(2))); } } @@ -142,7 +142,7 @@ mod maximum_independent_set { MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 100, 1]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); // Should select vertex 1 (weight 100) over vertices 0+2 (weight 2) assert_eq!(solutions[0], vec![0, 1, 0]); @@ -174,8 +174,7 @@ mod maximum_independent_set { #[test] fn test_direction() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); - assert_eq!(problem.direction(), Direction::Maximize); + let _problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); } #[test] @@ -200,7 +199,7 @@ mod maximum_independent_set { let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![]), vec![1i32; 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); // All vertices can be selected assert_eq!(solutions[0], vec![1, 1, 1]); @@ -215,8 +214,8 @@ mod maximum_independent_set { assert!(problem.evaluate(&[1, 0, 1]).is_valid()); assert!(problem.evaluate(&[0, 1, 0]).is_valid()); // Invalid configurations return Invalid - assert_eq!(problem.evaluate(&[1, 1, 0]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[0, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 0]), Max(None)); + assert_eq!(problem.evaluate(&[0, 1, 1]), Max(None)); } } @@ -251,10 +250,10 @@ mod minimum_vertex_cover { MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); // Valid: select vertex 1 (covers both edges) - assert_eq!(problem.evaluate(&[0, 1, 0]), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&[0, 1, 0]), Min(Some(1))); // Valid: select all vertices - assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(&[1, 1, 1]), Min(Some(3))); } #[test] @@ -263,10 +262,10 @@ mod minimum_vertex_cover { MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); // Invalid: no vertex selected - returns Invalid for minimization - assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 0]), Min(None)); // Invalid: only vertex 0 selected (edge 1-2 not covered) - assert_eq!(problem.evaluate(&[1, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 0]), Min(None)); } #[test] @@ -276,7 +275,7 @@ mod minimum_vertex_cover { MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![0, 1, 0]); } @@ -290,7 +289,7 @@ mod minimum_vertex_cover { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // There are 3 minimum covers of size 2 assert_eq!(solutions.len(), 3); for sol in &solutions { @@ -307,7 +306,7 @@ mod minimum_vertex_cover { MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![100, 1, 100]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); // Should select vertex 1 (weight 1) instead of 0 and 2 (total 200) assert_eq!(solutions[0], vec![0, 1, 0]); @@ -335,8 +334,7 @@ mod minimum_vertex_cover { #[test] fn test_direction() { - let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); - assert_eq!(problem.direction(), Direction::Minimize); + let _problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); } #[test] @@ -344,7 +342,7 @@ mod minimum_vertex_cover { let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![]), vec![1i32; 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // No edges means empty cover is valid and optimal assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![0, 0, 0]); @@ -355,7 +353,7 @@ mod minimum_vertex_cover { let problem = MinimumVertexCover::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Either vertex covers the single edge assert_eq!(solutions.len(), 2); } @@ -369,8 +367,8 @@ mod minimum_vertex_cover { assert!(problem.evaluate(&[0, 1, 0]).is_valid()); assert!(problem.evaluate(&[1, 0, 1]).is_valid()); // Invalid configurations return Invalid - assert_eq!(problem.evaluate(&[1, 0, 0]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[0, 0, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 0]), Min(None)); + assert_eq!(problem.evaluate(&[0, 0, 1]), Min(None)); } #[test] @@ -383,7 +381,7 @@ mod minimum_vertex_cover { let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(&is_problem); + let is_solutions = solver.find_all_witnesses(&is_problem); for is_sol in &is_solutions { // Complement should be a valid vertex cover let vc_config: Vec = is_sol.iter().map(|&x| 1 - x).collect(); @@ -490,7 +488,7 @@ mod kcoloring { let problem = KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // All solutions should be valid for sol in &solutions { assert!(problem.evaluate(sol)); @@ -503,7 +501,7 @@ mod kcoloring { let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); // All three vertices have different colors @@ -520,7 +518,7 @@ mod kcoloring { let solver = BruteForce::new(); // No satisfying assignments - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -547,7 +545,7 @@ mod kcoloring { let problem = KColoring::::new(SimpleGraph::new(3, vec![])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // Any coloring is valid when there are no edges assert!(problem.evaluate(&solutions[0])); } @@ -561,7 +559,7 @@ mod kcoloring { )); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); } diff --git a/src/unit_tests/models/algebraic/bmf.rs b/src/unit_tests/models/algebraic/bmf.rs index dac5a34be..30542d52a 100644 --- a/src/unit_tests/models/algebraic/bmf.rs +++ b/src/unit_tests/models/algebraic/bmf.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::Problem; +use crate::types::Min; include!("../../jl_helpers.rs"); @@ -80,11 +80,11 @@ fn test_evaluate() { // Exact factorization -> distance 0 let config = vec![1, 0, 0, 1, 1, 0, 0, 1]; - assert_eq!(Problem::evaluate(&problem, &config), SolutionSize::Valid(0)); + assert_eq!(Problem::evaluate(&problem, &config), Min(Some(0))); // Non-exact -> distance 2 let config = vec![0, 0, 0, 0, 0, 0, 0, 0]; - assert_eq!(Problem::evaluate(&problem, &config), SolutionSize::Valid(2)); + assert_eq!(Problem::evaluate(&problem, &config), Min(Some(2))); } #[test] @@ -94,10 +94,10 @@ fn test_brute_force_ones() { let problem = BMF::new(matrix, 1); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { // Exact factorization has distance 0 - assert_eq!(Problem::evaluate(&problem, sol), SolutionSize::Valid(0)); + assert_eq!(Problem::evaluate(&problem, sol), Min(Some(0))); } } @@ -108,7 +108,7 @@ fn test_brute_force_identity() { let problem = BMF::new(matrix, 2); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Should find exact factorization for sol in &solutions { assert!(problem.is_exact(sol)); @@ -122,7 +122,7 @@ fn test_brute_force_insufficient_rank() { let problem = BMF::new(matrix, 1); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Best approximation has distance > 0 let best_distance = problem.hamming_distance(&solutions[0]); // With rank 1, best we can do is distance 1 (all ones or all zeros except one) @@ -147,20 +147,13 @@ fn test_matrix_hamming_distance_function() { assert_eq!(matrix_hamming_distance(&a, &c), 0); } -#[test] -fn test_direction() { - let matrix = vec![vec![true]]; - let problem = BMF::new(matrix, 1); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_empty_matrix() { let matrix: Vec> = vec![]; let problem = BMF::new(matrix, 1); assert_eq!(problem.num_variables(), 0); // Empty matrix has distance 0 - assert_eq!(Problem::evaluate(&problem, &[]), SolutionSize::Valid(0)); + assert_eq!(Problem::evaluate(&problem, &[]), Min(Some(0))); } #[test] @@ -173,8 +166,7 @@ fn test_is_exact() { #[test] fn test_bmf_problem() { - use crate::traits::{OptimizationProblem, Problem}; - use crate::types::Direction; + use crate::traits::Problem; // 2x2 identity matrix with rank 2 let matrix = vec![vec![true, false], vec![false, true]]; @@ -187,24 +179,23 @@ fn test_bmf_problem() { // Config: [1,0,0,1, 1,0,0,1] assert_eq!( Problem::evaluate(&problem, &[1, 0, 0, 1, 1, 0, 0, 1]), - SolutionSize::Valid(0) + Min(Some(0)) ); // All zeros -> product is all zeros, distance = 2 assert_eq!( Problem::evaluate(&problem, &[0, 0, 0, 0, 0, 0, 0, 0]), - SolutionSize::Valid(2) + Min(Some(2)) ); - // Direction is minimize - assert_eq!(problem.direction(), Direction::Minimize); + // ExtremumSense is minimize // Test with 1x1 matrix let matrix = vec![vec![true]]; let problem = BMF::new(matrix, 1); assert_eq!(problem.dims(), vec![2; 2]); // B(1*1) + C(1*1) - assert_eq!(Problem::evaluate(&problem, &[1, 1]), SolutionSize::Valid(0)); // Exact - assert_eq!(Problem::evaluate(&problem, &[0, 0]), SolutionSize::Valid(1)); // Distance 1 + assert_eq!(Problem::evaluate(&problem, &[1, 1]), Min(Some(0))); // Exact + assert_eq!(Problem::evaluate(&problem, &[0, 0]), Min(Some(1))); // Distance 1 } #[test] @@ -229,15 +220,15 @@ fn test_jl_parity_evaluation() { let config = jl_parse_config(&eval["config"]); let result = problem.evaluate(&config); let jl_size = eval["size"].as_i64().unwrap() as i32; - // BMF always returns Valid(hamming_distance) + // BMF always returns Min(hamming_distance). assert_eq!( result, - SolutionSize::Valid(jl_size), + Min(Some(jl_size)), "BMF: size mismatch for config {:?}", config ); } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "BMF best solutions mismatch"); @@ -269,9 +260,9 @@ fn test_bmf_paper_example() { // C: c00=1,c01=1,c02=0, c10=0,c11=1,c12=1 let config = vec![1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1]; let result = Problem::evaluate(&problem, &config); - assert_eq!(result, SolutionSize::Valid(0)); // exact factorization + assert_eq!(result, Min(Some(0))); // exact factorization let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); - assert_eq!(Problem::evaluate(&problem, &best), SolutionSize::Valid(0)); + let best = solver.find_witness(&problem).unwrap(); + assert_eq!(Problem::evaluate(&problem, &best), Min(Some(0))); } diff --git a/src/unit_tests/models/algebraic/closest_vector_problem.rs b/src/unit_tests/models/algebraic/closest_vector_problem.rs index d4b2cd5ff..ff5e41dc4 100644 --- a/src/unit_tests/models/algebraic/closest_vector_problem.rs +++ b/src/unit_tests/models/algebraic/closest_vector_problem.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::Problem; +use crate::types::Min; #[test] fn test_cvp_creation() { @@ -35,16 +35,7 @@ fn test_cvp_evaluate() { // config offset: x_i - lower = 1 - (-2) = 3 let config_111 = vec![3, 3, 3]; // maps to x=(1,1,1) let result = Problem::evaluate(&cvp, &config_111); - assert_eq!(result, SolutionSize::Valid(1.0)); -} - -#[test] -fn test_cvp_direction() { - let basis = vec![vec![1, 0], vec![0, 1]]; - let target = vec![0.5, 0.5]; - let bounds = vec![VarBounds::bounded(0, 2), VarBounds::bounded(0, 2)]; - let cvp = ClosestVectorProblem::new(basis, target, bounds); - assert_eq!(cvp.direction(), Direction::Minimize); + assert_eq!(result, Min(Some(1.0))); } #[test] @@ -103,14 +94,14 @@ fn test_cvp_brute_force() { let cvp = ClosestVectorProblem::new(basis, target, bounds); let solver = BruteForce::new(); - let solution = solver.find_best(&cvp).expect("should find a solution"); + let solution = solver.find_witness(&cvp).expect("should find a solution"); let values: Vec = solution .iter() .enumerate() .map(|(i, &c)| cvp.bounds()[i].lower.unwrap() + c as i64) .collect(); assert_eq!(values, vec![1, 1, 1]); - assert_eq!(Problem::evaluate(&cvp, &solution), SolutionSize::Valid(1.0)); + assert_eq!(Problem::evaluate(&cvp, &solution), Min(Some(1.0))); } #[test] @@ -145,7 +136,7 @@ fn test_cvp_f64_basis() { let cvp = ClosestVectorProblem::new(basis, target, bounds); let solver = BruteForce::new(); - let solution = solver.find_best(&cvp).expect("should find a solution"); + let solution = solver.find_witness(&cvp).expect("should find a solution"); let values: Vec = solution .iter() .enumerate() @@ -169,7 +160,7 @@ fn test_cvp_2d_identity() { let cvp = ClosestVectorProblem::new(basis, target, bounds); let solver = BruteForce::new(); - let solution = solver.find_best(&cvp).expect("should find a solution"); + let solution = solver.find_witness(&cvp).expect("should find a solution"); let values: Vec = solution .iter() .enumerate() @@ -189,7 +180,7 @@ fn test_cvp_evaluate_exact_solution() { // x=(2,2), Bx=(2,2), distance=0 let config = vec![2, 2]; // offset from lower=0 let result = Problem::evaluate(&cvp, &config); - assert_eq!(result, SolutionSize::Valid(0.0)); + assert_eq!(result, Min(Some(0.0))); } #[test] @@ -228,7 +219,7 @@ fn test_cvp_paper_example() { assert!((dist - 0.29_f64.sqrt()).abs() < 1e-10); let solver = BruteForce::new(); - let best = solver.find_best(&cvp).unwrap(); + let best = solver.find_witness(&cvp).unwrap(); let best_dist = Problem::evaluate(&cvp, &best).unwrap(); assert!((best_dist - 0.29_f64.sqrt()).abs() < 1e-10); } diff --git a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs index 7cb9e5d2f..60fbba417 100644 --- a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs +++ b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs @@ -60,7 +60,7 @@ fn test_consecutive_block_minimization_brute_force() { 2, ); let solver = BruteForce::new(); - let mut solutions = solver.find_all_satisfying(&problem); + let mut solutions = solver.find_all_witnesses(&problem); solutions.sort(); let mut expected = vec![vec![0, 2, 1], vec![1, 2, 0]]; expected.sort(); diff --git a/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs b/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs index 4b59c2dc6..16fc52b93 100644 --- a/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs +++ b/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_yes_matrix() -> Vec> { @@ -51,7 +51,7 @@ fn test_consecutive_ones_matrix_augmentation_no_instance() { let problem = ConsecutiveOnesMatrixAugmentation::new(issue_no_matrix(), 0); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] diff --git a/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs b/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs index d245c3fcb..e1efe8ca5 100644 --- a/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs +++ b/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; /// Tucker matrix (3×4) — the classic C1P obstruction. @@ -70,7 +70,7 @@ fn test_consecutive_ones_submatrix_brute_force() { let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -79,7 +79,7 @@ fn test_consecutive_ones_submatrix_brute_force() { fn test_consecutive_ones_submatrix_brute_force_all() { let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -91,7 +91,7 @@ fn test_consecutive_ones_submatrix_unsatisfiable() { // Tucker matrix with K=4: no permutation of all 4 columns gives C1P let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 4); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -104,9 +104,7 @@ fn test_consecutive_ones_submatrix_trivial_c1p() { ]; let problem = ConsecutiveOnesSubmatrix::new(matrix, 3); let solver = BruteForce::new(); - let solution = solver - .find_satisfying(&problem) - .expect("full matrix has C1P"); + let solution = solver.find_witness(&problem).expect("full matrix has C1P"); assert!(problem.evaluate(&solution)); } @@ -116,7 +114,7 @@ fn test_consecutive_ones_submatrix_single_column() { let matrix = vec![vec![true, false, true], vec![false, true, false]]; let problem = ConsecutiveOnesSubmatrix::new(matrix, 1); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 3); // each column works individually } @@ -130,7 +128,7 @@ fn test_consecutive_ones_submatrix_empty_rows() { ]; let problem = ConsecutiveOnesSubmatrix::new(matrix, 2); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -166,7 +164,7 @@ fn test_consecutive_ones_submatrix_paper_example() { assert!(problem.evaluate(&[1, 1, 0, 1])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // All solutions must be valid for sol in &solutions { assert!(problem.evaluate(sol)); diff --git a/src/unit_tests/models/algebraic/ilp.rs b/src/unit_tests/models/algebraic/ilp.rs index 91f60cc27..d993f0716 100644 --- a/src/unit_tests/models/algebraic/ilp.rs +++ b/src/unit_tests/models/algebraic/ilp.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Extremum; // ============================================================ // Comparison tests @@ -97,17 +97,6 @@ fn test_linear_constraint_out_of_bounds() { // ObjectiveSense tests // ============================================================ -#[test] -fn test_objective_sense_direction_conversions() { - // Test that ObjectiveSense and Direction can be converted - let max_sense = ObjectiveSense::Maximize; - let min_sense = ObjectiveSense::Minimize; - - // Direction values match ObjectiveSense semantics - assert_eq!(max_sense, ObjectiveSense::Maximize); - assert_eq!(min_sense, ObjectiveSense::Minimize); -} - // ============================================================ // ILP tests // ============================================================ @@ -189,15 +178,6 @@ fn test_ilp_num_variables() { assert_eq!(ilp.num_variables(), 5); } -#[test] -fn test_ilp_direction() { - let max_ilp = ILP::::new(2, vec![], vec![], ObjectiveSense::Maximize); - let min_ilp = ILP::::new(2, vec![], vec![], ObjectiveSense::Minimize); - - assert_eq!(max_ilp.direction(), Direction::Maximize); - assert_eq!(min_ilp.direction(), Direction::Minimize); -} - #[test] fn test_ilp_evaluate_valid() { // Maximize x0 + 2*x1 subject to x0 + x1 <= 1 @@ -209,10 +189,16 @@ fn test_ilp_evaluate_valid() { ); // Config [0, 1] means x0=0, x1=1 => obj = 2, valid - assert_eq!(Problem::evaluate(&ilp, &[0, 1]), SolutionSize::Valid(2.0)); + assert_eq!( + Problem::evaluate(&ilp, &[0, 1]), + Extremum::maximize(Some(2.0)) + ); // Config [1, 0] means x0=1, x1=0 => obj = 1, valid - assert_eq!(Problem::evaluate(&ilp, &[1, 0]), SolutionSize::Valid(1.0)); + assert_eq!( + Problem::evaluate(&ilp, &[1, 0]), + Extremum::maximize(Some(1.0)) + ); } #[test] @@ -226,7 +212,7 @@ fn test_ilp_evaluate_invalid() { ); // Config [1, 1] means x0=1, x1=1 => invalid (1+1 > 1), returns Invalid - assert_eq!(Problem::evaluate(&ilp, &[1, 1]), SolutionSize::Invalid); + assert_eq!(Problem::evaluate(&ilp, &[1, 1]), Extremum::maximize(None)); } #[test] @@ -240,7 +226,7 @@ fn test_ilp_brute_force_maximization() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&ilp); + let solutions = solver.find_all_witnesses(&ilp); // Optimal: x1=1, x0=0 => objective = 2 assert_eq!(solutions.len(), 1); @@ -258,12 +244,12 @@ fn test_ilp_brute_force_minimization() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&ilp); + let solutions = solver.find_all_witnesses(&ilp); // Optimal: x0=1,x1=0 or x0=0,x1=1 => objective = 1 assert_eq!(solutions.len(), 2); for sol in &solutions { - assert_eq!(Problem::evaluate(&ilp, sol), SolutionSize::Valid(1.0)); + assert_eq!(Problem::evaluate(&ilp, sol), Extremum::minimize(Some(1.0))); } } @@ -281,7 +267,7 @@ fn test_ilp_brute_force_no_feasible() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&ilp); + let solutions = solver.find_all_witnesses(&ilp); // All solutions are infeasible - BruteForce should return empty list assert!( @@ -291,7 +277,7 @@ fn test_ilp_brute_force_no_feasible() { // Verify all configs are indeed infeasible for config in &[[0], [1]] { - assert_eq!(Problem::evaluate(&ilp, config), SolutionSize::Invalid); + assert_eq!(Problem::evaluate(&ilp, config), Extremum::minimize(None)); let values = ilp.config_to_values(config); assert!(!ilp.is_feasible(&values)); } @@ -308,7 +294,7 @@ fn test_ilp_unconstrained() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&ilp); + let solutions = solver.find_all_witnesses(&ilp); // Optimal: both = 1 assert_eq!(solutions.len(), 1); @@ -326,7 +312,7 @@ fn test_ilp_equality_constraint() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&ilp); + let solutions = solver.find_all_witnesses(&ilp); // Optimal: x0=0, x1=1 => objective = 0 assert_eq!(solutions.len(), 1); @@ -350,7 +336,7 @@ fn test_ilp_multiple_constraints() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&ilp); + let solutions = solver.find_all_witnesses(&ilp); // Optimal: x0=1, x1=0, x2=1 => objective = 2 assert_eq!(solutions.len(), 1); @@ -379,15 +365,22 @@ fn test_ilp_problem() { assert_eq!(ilp.dims(), vec![2, 2]); // [0, 0] -> feasible, obj = 0 - assert_eq!(Problem::evaluate(&ilp, &[0, 0]), SolutionSize::Valid(0.0)); + assert_eq!( + Problem::evaluate(&ilp, &[0, 0]), + Extremum::maximize(Some(0.0)) + ); // [0, 1] -> feasible, obj = 2 - assert_eq!(Problem::evaluate(&ilp, &[0, 1]), SolutionSize::Valid(2.0)); + assert_eq!( + Problem::evaluate(&ilp, &[0, 1]), + Extremum::maximize(Some(2.0)) + ); // [1, 0] -> feasible, obj = 1 - assert_eq!(Problem::evaluate(&ilp, &[1, 0]), SolutionSize::Valid(1.0)); + assert_eq!( + Problem::evaluate(&ilp, &[1, 0]), + Extremum::maximize(Some(1.0)) + ); // [1, 1] -> infeasible - assert_eq!(Problem::evaluate(&ilp, &[1, 1]), SolutionSize::Invalid); - - assert_eq!(ilp.direction(), Direction::Maximize); + assert_eq!(Problem::evaluate(&ilp, &[1, 1]), Extremum::maximize(None)); } #[test] @@ -399,9 +392,14 @@ fn test_ilp_problem_minimize() { vec![(0, 1.0), (1, 1.0)], ObjectiveSense::Minimize, ); - assert_eq!(Problem::evaluate(&ilp, &[0, 0]), SolutionSize::Valid(0.0)); - assert_eq!(Problem::evaluate(&ilp, &[1, 1]), SolutionSize::Valid(2.0)); - assert_eq!(ilp.direction(), Direction::Minimize); + assert_eq!( + Problem::evaluate(&ilp, &[0, 0]), + Extremum::minimize(Some(0.0)) + ); + assert_eq!( + Problem::evaluate(&ilp, &[1, 1]), + Extremum::minimize(Some(2.0)) + ); } #[test] @@ -443,7 +441,7 @@ fn test_ilp_paper_example() { // Verify optimal solution x* = (3, 2) → config [3, 2] let result = Problem::evaluate(&ilp, &[3, 2]); - assert_eq!(result, SolutionSize::Valid(-27.0)); + assert_eq!(result, Extremum::minimize(Some(-27.0))); // Verify feasibility: 3+2=5≤5, 4*3+7*2=26≤28 assert!(ilp.is_feasible(&[3, 2])); @@ -453,5 +451,5 @@ fn test_ilp_paper_example() { // Verify suboptimal feasible point: -5*0 - 6*4 = -24 > -27 let result2 = Problem::evaluate(&ilp, &[0, 4]); - assert_eq!(result2, SolutionSize::Valid(-24.0)); + assert_eq!(result2, Extremum::minimize(Some(-24.0))); } diff --git a/src/unit_tests/models/algebraic/quadratic_assignment.rs b/src/unit_tests/models/algebraic/quadratic_assignment.rs index 703321938..283c49e63 100644 --- a/src/unit_tests/models/algebraic/quadratic_assignment.rs +++ b/src/unit_tests/models/algebraic/quadratic_assignment.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::Problem; +use crate::types::Min; /// Create a 4x4 test instance matching issue #300's example. /// @@ -51,10 +51,7 @@ fn test_quadratic_assignment_evaluate_identity() { // cost = sum_{i != j} C[i][j] * D[i][j] // = 5*4 + 2*1 + 0*1 + 5*4 + 0*3 + 3*4 + 2*1 + 0*3 + 4*4 + 0*1 + 3*4 + 4*4 // = 20 + 2 + 0 + 20 + 0 + 12 + 2 + 0 + 16 + 0 + 12 + 16 = 100 - assert_eq!( - Problem::evaluate(&qap, &[0, 1, 2, 3]), - SolutionSize::Valid(100) - ); + assert_eq!(Problem::evaluate(&qap, &[0, 1, 2, 3]), Min(Some(100))); } #[test] @@ -67,38 +64,20 @@ fn test_quadratic_assignment_evaluate_swap() { // i=2,j=0: 2*D[1][0]=2*4=8 i=2,j=1: 0*D[1][2]=0*3=0 i=2,j=3: 4*D[1][3]=4*4=16 // i=3,j=0: 0*D[3][0]=0 i=3,j=1: 3*D[3][2]=3*4=12 i=3,j=2: 4*D[3][1]=4*4=16 // Total = 5+8+0+5+0+12+8+0+16+0+12+16 = 82 - assert_eq!( - Problem::evaluate(&qap, &[0, 2, 1, 3]), - SolutionSize::Valid(82) - ); + assert_eq!(Problem::evaluate(&qap, &[0, 2, 1, 3]), Min(Some(82))); } #[test] fn test_quadratic_assignment_evaluate_invalid() { let qap = make_test_instance(); // Duplicate location 0 — not injective, should be Invalid. - assert_eq!( - Problem::evaluate(&qap, &[0, 0, 1, 2]), - SolutionSize::Invalid - ); + assert_eq!(Problem::evaluate(&qap, &[0, 0, 1, 2]), Min(None)); // Out-of-range location index. - assert_eq!( - Problem::evaluate(&qap, &[0, 1, 2, 99]), - SolutionSize::Invalid - ); + assert_eq!(Problem::evaluate(&qap, &[0, 1, 2, 99]), Min(None)); // Wrong config length — too short. - assert_eq!(Problem::evaluate(&qap, &[0, 1, 2]), SolutionSize::Invalid); + assert_eq!(Problem::evaluate(&qap, &[0, 1, 2]), Min(None)); // Wrong config length — too long. - assert_eq!( - Problem::evaluate(&qap, &[0, 1, 2, 3, 0]), - SolutionSize::Invalid - ); -} - -#[test] -fn test_quadratic_assignment_direction() { - let qap = make_test_instance(); - assert_eq!(qap.direction(), Direction::Minimize); + assert_eq!(Problem::evaluate(&qap, &[0, 1, 2, 3, 0]), Min(None)); } #[test] @@ -125,13 +104,13 @@ fn test_quadratic_assignment_rectangular() { assert_eq!(qap.num_locations(), 3); assert_eq!(qap.dims(), vec![3, 3]); // Assignment f=(0,1): cost = C[0][1]*D[0][1] + C[1][0]*D[1][0] = 3*1 + 3*1 = 6 - assert_eq!(Problem::evaluate(&qap, &[0, 1]), SolutionSize::Valid(6)); + assert_eq!(Problem::evaluate(&qap, &[0, 1]), Min(Some(6))); // Assignment f=(0,2): cost = 3*D[0][2] + 3*D[2][0] = 3*4 + 3*4 = 24 - assert_eq!(Problem::evaluate(&qap, &[0, 2]), SolutionSize::Valid(24)); + assert_eq!(Problem::evaluate(&qap, &[0, 2]), Min(Some(24))); // BruteForce should find optimal let solver = BruteForce::new(); - let best = solver.find_best(&qap).unwrap(); - assert_eq!(Problem::evaluate(&qap, &best), SolutionSize::Valid(6)); + let best = solver.find_witness(&qap).unwrap(); + assert_eq!(Problem::evaluate(&qap, &best), Min(Some(6))); } #[test] @@ -153,12 +132,9 @@ fn test_quadratic_assignment_too_many_facilities() { fn test_quadratic_assignment_solver() { let qap = make_test_instance(); let solver = BruteForce::new(); - let best = solver.find_best(&qap); + let best = solver.find_witness(&qap); assert!(best.is_some()); let best_config = best.unwrap(); // The brute-force solver finds the optimal assignment f* = (3, 0, 1, 2) with cost 56. - assert_eq!( - Problem::evaluate(&qap, &best_config), - SolutionSize::Valid(56) - ); + assert_eq!(Problem::evaluate(&qap, &best_config), Min(Some(56))); } diff --git a/src/unit_tests/models/algebraic/qubo.rs b/src/unit_tests/models/algebraic/qubo.rs index d33f5b958..83ebae164 100644 --- a/src/unit_tests/models/algebraic/qubo.rs +++ b/src/unit_tests/models/algebraic/qubo.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::Problem; +use crate::types::Min; include!("../../jl_helpers.rs"); #[test] @@ -21,12 +21,6 @@ fn test_qubo_new() { assert_eq!(problem.get(0, 1), Some(&3.0)); } -#[test] -fn test_direction() { - let problem = QUBO::::from_matrix(vec![vec![1.0]]); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_num_variables() { let problem = QUBO::::from_matrix(vec![vec![0.0; 5]; 5]); @@ -49,7 +43,7 @@ fn test_matrix_access() { fn test_empty_qubo() { let problem = QUBO::::from_matrix(vec![]); assert_eq!(problem.num_vars(), 0); - assert_eq!(Problem::evaluate(&problem, &[]), SolutionSize::Valid(0.0)); + assert_eq!(Problem::evaluate(&problem, &[]), Min(Some(0.0))); } #[test] @@ -94,7 +88,7 @@ fn test_jl_parity_evaluation() { let problem = QUBO::from_matrix(rust_matrix); for eval in instance["evaluations"].as_array().unwrap() { let config = jl_parse_config(&eval["config"]); - let result: SolutionSize = Problem::evaluate(&problem, &config); + let result = Problem::evaluate(&problem, &config); let jl_size = eval["size"].as_f64().unwrap(); assert!(result.is_valid(), "QUBO should always be valid"); assert!( @@ -103,7 +97,7 @@ fn test_jl_parity_evaluation() { config ); } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "QUBO best solutions mismatch"); @@ -118,15 +112,9 @@ fn test_qubo_paper_example() { vec![0.0, -1.0, 2.0], vec![0.0, 0.0, -1.0], ]); - assert_eq!( - Problem::evaluate(&problem, &[1, 0, 1]), - SolutionSize::Valid(-2.0) - ); + assert_eq!(Problem::evaluate(&problem, &[1, 0, 1]), Min(Some(-2.0))); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); - assert_eq!( - Problem::evaluate(&problem, &best), - SolutionSize::Valid(-2.0) - ); + let best = solver.find_witness(&problem).unwrap(); + assert_eq!(Problem::evaluate(&problem, &best), Min(Some(-2.0))); } diff --git a/src/unit_tests/models/algebraic/sparse_matrix_compression.rs b/src/unit_tests/models/algebraic/sparse_matrix_compression.rs index a6095d630..2d3b48630 100644 --- a/src/unit_tests/models/algebraic/sparse_matrix_compression.rs +++ b/src/unit_tests/models/algebraic/sparse_matrix_compression.rs @@ -1,6 +1,6 @@ use super::*; use crate::registry::VariantEntry; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_example_matrix() -> Vec> { @@ -67,11 +67,11 @@ fn test_sparse_matrix_compression_bruteforce_finds_unique_solution() { let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("issue example should be satisfiable"); assert_eq!(solution, vec![1, 1, 1, 0]); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all, vec![vec![1, 1, 1, 0]]); } diff --git a/src/unit_tests/models/formula/circuit.rs b/src/unit_tests/models/formula/circuit.rs index 81a9e1899..137829ba9 100644 --- a/src/unit_tests/models/formula/circuit.rs +++ b/src/unit_tests/models/formula/circuit.rs @@ -156,7 +156,7 @@ fn test_circuit_sat_brute_force() { let problem = CircuitSAT::new(circuit); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // All satisfying: c matches x AND y // 4 valid configs: (0,0,0), (0,0,1), (0,1,0), (1,1,1) assert_eq!(solutions.len(), 4); @@ -182,7 +182,7 @@ fn test_circuit_sat_complex() { let problem = CircuitSAT::new(circuit); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // All valid solutions satisfy both assignments for sol in &solutions { assert!(problem.evaluate(sol)); @@ -290,6 +290,6 @@ fn test_circuit_sat_paper_example() { // All 4 consistent configs are satisfying (CircuitSAT checks consistency) let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 4); } diff --git a/src/unit_tests/models/formula/ksat.rs b/src/unit_tests/models/formula/ksat.rs index ebaa569a8..eaad41707 100644 --- a/src/unit_tests/models/formula/ksat.rs +++ b/src/unit_tests/models/formula/ksat.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; use crate::variant::{K2, K3, KN}; include!("../../jl_helpers.rs"); @@ -60,7 +60,7 @@ fn test_3sat_brute_force() { ], ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { @@ -173,7 +173,7 @@ fn test_jl_parity_evaluation() { config ); } - let rust_best = BruteForce::new().find_all_satisfying(&problem); + let rust_best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best_set: HashSet> = rust_best.into_iter().collect(); assert_eq!(rust_best_set, jl_best, "KSat best solutions mismatch"); @@ -241,6 +241,6 @@ fn test_ksat_paper_example() { assert!(problem.evaluate(&[1, 0, 1])); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); } diff --git a/src/unit_tests/models/formula/nae_satisfiability.rs b/src/unit_tests/models/formula/nae_satisfiability.rs index 9b38f88f5..51fcf6e7e 100644 --- a/src/unit_tests/models/formula/nae_satisfiability.rs +++ b/src/unit_tests/models/formula/nae_satisfiability.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; use std::collections::HashSet; @@ -64,7 +64,7 @@ fn test_nae_solver_counts_ten_solutions_for_issue_example() { let problem = issue_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); let set: HashSet> = solutions.into_iter().collect(); assert_eq!(set.len(), 10); @@ -78,9 +78,9 @@ fn test_nae_empty_formula_is_trivially_satisfying() { let solver = BruteForce::new(); assert!(problem.evaluate(&[])); - assert_eq!(solver.find_satisfying(&problem), Some(vec![])); + assert_eq!(solver.find_witness(&problem), Some(vec![])); assert_eq!( - solver.find_all_satisfying(&problem), + solver.find_all_witnesses(&problem), vec![Vec::::new()] ); } @@ -139,5 +139,5 @@ fn test_nae_satisfiability_paper_example() { assert!(problem.evaluate(&[0, 0, 0, 1, 1])); assert!(problem.evaluate(&[1, 1, 1, 0, 0])); - assert_eq!(solver.find_all_satisfying(&problem).len(), 10); + assert_eq!(solver.find_all_witnesses(&problem).len(), 10); } diff --git a/src/unit_tests/models/formula/qbf.rs b/src/unit_tests/models/formula/qbf.rs index eccd7d28b..136cc967d 100644 --- a/src/unit_tests/models/formula/qbf.rs +++ b/src/unit_tests/models/formula/qbf.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -150,7 +150,7 @@ fn test_qbf_solver() { let solver = BruteForce::new(); // With dims()=[], there is exactly one config: []. evaluate([]) = is_true() = true - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let sol = solution.unwrap(); assert_eq!(sol, Vec::::new()); @@ -167,7 +167,7 @@ fn test_qbf_solver_false() { ); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -181,7 +181,7 @@ fn test_qbf_solver_all_satisfying() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // Only one config exists (the empty config []), and it satisfies assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], Vec::::new()); diff --git a/src/unit_tests/models/formula/sat.rs b/src/unit_tests/models/formula/sat.rs index cd78576f7..29ddad85a 100644 --- a/src/unit_tests/models/formula/sat.rs +++ b/src/unit_tests/models/formula/sat.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; include!("../../jl_helpers.rs"); @@ -97,9 +97,9 @@ fn test_empty_formula_zero_vars_solver() { let problem = Satisfiability::new(0, vec![]); let solver = BruteForce::new(); - assert_eq!(solver.find_satisfying(&problem), Some(vec![])); + assert_eq!(solver.find_witness(&problem), Some(vec![])); assert_eq!( - solver.find_all_satisfying(&problem), + solver.find_all_witnesses(&problem), vec![Vec::::new()] ); } @@ -109,8 +109,8 @@ fn test_zero_vars_unsat_solver() { let problem = Satisfiability::new(0, vec![CNFClause::new(vec![1])]); let solver = BruteForce::new(); - assert_eq!(solver.find_satisfying(&problem), None); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert_eq!(solver.find_witness(&problem), None); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] @@ -119,7 +119,7 @@ fn test_single_literal_clauses() { let problem = Satisfiability::new(2, vec![CNFClause::new(vec![1]), CNFClause::new(vec![-2])]); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![1, 0]); // x1=T, x2=F } @@ -186,7 +186,7 @@ fn test_jl_parity_evaluation() { config ); } - let rust_best = BruteForce::new().find_all_satisfying(&problem); + let rust_best = BruteForce::new().find_all_witnesses(&problem); let rust_best_set: HashSet> = rust_best.into_iter().collect(); if !rust_best_set.is_empty() { let jl_best = jl_parse_configs_set(&instance["best_solutions"]); @@ -223,6 +223,6 @@ fn test_sat_paper_example() { assert!(problem.evaluate(&[1, 0, 1])); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); } diff --git a/src/unit_tests/models/graph/acyclic_partition.rs b/src/unit_tests/models/graph/acyclic_partition.rs index c15582f30..70e1d7df8 100644 --- a/src/unit_tests/models/graph/acyclic_partition.rs +++ b/src/unit_tests/models/graph/acyclic_partition.rs @@ -1,6 +1,6 @@ use super::*; use crate::registry::declared_size_fields; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; use crate::traits::Problem; use serde_json; @@ -160,7 +160,7 @@ fn test_acyclic_partition_solver_finds_issue_example() { let problem = yes_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } @@ -168,7 +168,7 @@ fn test_acyclic_partition_solver_finds_issue_example() { #[test] fn test_acyclic_partition_solver_has_four_canonical_solutions() { let problem = yes_instance(); - let solutions = BruteForce::new().find_all_satisfying(&problem); + let solutions = BruteForce::new().find_all_witnesses(&problem); let normalized: BTreeSet> = solutions .iter() .map(|config| canonicalize_labels(config)) @@ -187,7 +187,7 @@ fn test_acyclic_partition_solver_has_four_canonical_solutions() { #[test] fn test_acyclic_partition_no_solution_when_cost_bound_is_four() { let problem = no_cost_instance(); - assert!(BruteForce::new().find_satisfying(&problem).is_none()); + assert!(BruteForce::new().find_witness(&problem).is_none()); } #[test] diff --git a/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs b/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs index 843da8994..2faab061f 100644 --- a/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs +++ b/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::BipartiteGraph; use crate::traits::Problem; @@ -102,11 +102,11 @@ fn test_balanced_complete_bipartite_subgraph_solver_yes_instance() { let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_2_graph(), 3); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all, vec![issue_instance_2_witness()]); } @@ -115,7 +115,7 @@ fn test_balanced_complete_bipartite_subgraph_solver_no_instance() { let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 3); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -155,5 +155,5 @@ fn test_balanced_complete_bipartite_subgraph_paper_example() { let solver = BruteForce::new(); assert!(problem.evaluate(&witness)); - assert_eq!(solver.find_all_satisfying(&problem), vec![witness]); + assert_eq!(solver.find_all_witnesses(&problem), vec![witness]); } diff --git a/src/unit_tests/models/graph/biclique_cover.rs b/src/unit_tests/models/graph/biclique_cover.rs index 1d48479af..b4560be6e 100644 --- a/src/unit_tests/models/graph/biclique_cover.rs +++ b/src/unit_tests/models/graph/biclique_cover.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::BipartiteGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Min; include!("../../jl_helpers.rs"); @@ -74,10 +74,10 @@ fn test_evaluate() { let problem = BicliqueCover::new(graph, 1); // Valid cover with size 2 - assert_eq!(problem.evaluate(&[1, 0, 1, 0]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[1, 0, 1, 0]), Min(Some(2))); // Invalid cover returns Invalid - assert_eq!(problem.evaluate(&[1, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 0, 0]), Min(None)); } #[test] @@ -87,7 +87,7 @@ fn test_brute_force_simple() { let problem = BicliqueCover::new(graph, 1); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.is_valid_cover(sol)); // Minimum size is 2 (one left, one right vertex) @@ -103,7 +103,7 @@ fn test_brute_force_two_bicliques() { let problem = BicliqueCover::new(graph, 2); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.is_valid_cover(sol)); } @@ -135,25 +135,17 @@ fn test_is_biclique_cover_function() { assert!(!is_biclique_cover(&edges, &left, &right)); } -#[test] -fn test_direction() { - let graph = BipartiteGraph::new(1, 1, vec![(0, 0)]); - let problem = BicliqueCover::new(graph, 1); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_empty_edges() { let graph = BipartiteGraph::new(2, 2, vec![]); let problem = BicliqueCover::new(graph, 1); // No edges to cover -> valid with size 0 - assert_eq!(problem.evaluate(&[0, 0, 0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0, 0, 0, 0]), Min(Some(0))); } #[test] fn test_biclique_problem() { - use crate::traits::{OptimizationProblem, Problem}; - use crate::types::Direction; + use crate::traits::Problem; // Single edge (0,0) in local coords with k=1, 2 left + 2 right vertices let graph = BipartiteGraph::new(2, 2, vec![(0, 0)]); @@ -164,27 +156,23 @@ fn test_biclique_problem() { // Valid cover: vertex 0 and vertex 2 in biclique 0 // Config: [v0_b0=1, v1_b0=0, v2_b0=1, v3_b0=0] - assert_eq!(problem.evaluate(&[1, 0, 1, 0]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[1, 0, 1, 0]), Min(Some(2))); // Invalid cover: only vertex 0, edge (0,2) not covered - assert_eq!(problem.evaluate(&[1, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 0, 0]), Min(None)); // Valid cover with all vertices -> size 4 - assert_eq!(problem.evaluate(&[1, 1, 1, 1]), SolutionSize::Valid(4)); + assert_eq!(problem.evaluate(&[1, 1, 1, 1]), Min(Some(4))); // Empty config: no vertices in biclique, edge not covered - assert_eq!(problem.evaluate(&[0, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 0, 0]), Min(None)); - // Direction is minimize - assert_eq!(problem.direction(), Direction::Minimize); + // ExtremumSense is minimize // Test with no edges: any config is valid let empty_graph = BipartiteGraph::new(2, 2, vec![]); let empty_problem = BicliqueCover::new(empty_graph, 1); - assert_eq!( - empty_problem.evaluate(&[0, 0, 0, 0]), - SolutionSize::Valid(0) - ); + assert_eq!(empty_problem.evaluate(&[0, 0, 0, 0]), Min(Some(0))); } #[test] @@ -213,18 +201,18 @@ fn test_jl_parity_evaluation() { if jl_valid { assert_eq!( result, - SolutionSize::Valid(jl_size), + Min(Some(jl_size)), "BicliqueCover: valid config mismatch" ); } else { assert_eq!( result, - SolutionSize::Invalid, + Min(None), "BicliqueCover: invalid config should be Invalid" ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "BicliqueCover best solutions mismatch"); @@ -269,7 +257,7 @@ fn test_biclique_paper_example() { assert_eq!(result.unwrap(), 6); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); let best_size = problem.evaluate(&best).unwrap(); assert!(best_size <= 6); } diff --git a/src/unit_tests/models/graph/biconnectivity_augmentation.rs b/src/unit_tests/models/graph/biconnectivity_augmentation.rs index c71b2f102..db4f33fc7 100644 --- a/src/unit_tests/models/graph/biconnectivity_augmentation.rs +++ b/src/unit_tests/models/graph/biconnectivity_augmentation.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::One; @@ -90,11 +90,11 @@ fn test_biconnectivity_augmentation_solver() { let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("expected a satisfying augmentation"); assert_eq!(solution, vec![0, 0, 1]); - let all_solutions = solver.find_all_satisfying(&problem); + let all_solutions = solver.find_all_witnesses(&problem); assert_eq!(all_solutions, vec![vec![0, 0, 1]]); } @@ -103,8 +103,8 @@ fn test_biconnectivity_augmentation_no_solution() { let problem = BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 2, 1)], 1); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_witness(&problem).is_none()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] @@ -112,7 +112,7 @@ fn test_biconnectivity_augmentation_paper_example() { let problem = example_instance(); let solver = BruteForce::new(); let satisfying_config = vec![1, 0, 0, 1, 0, 0, 1, 0, 1]; - let satisfying_solutions = solver.find_all_satisfying(&problem); + let satisfying_solutions = solver.find_all_witnesses(&problem); assert!(problem.evaluate(&satisfying_config)); assert!(satisfying_solutions.contains(&satisfying_config)); @@ -133,7 +133,7 @@ fn test_biconnectivity_augmentation_paper_example() { 3, ); assert!(!over_budget_problem.evaluate(&satisfying_config)); - assert!(solver.find_satisfying(&over_budget_problem).is_none()); + assert!(solver.find_witness(&over_budget_problem).is_none()); } #[test] diff --git a/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs b/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs index a82530101..e8f72c0df 100644 --- a/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs +++ b/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs @@ -1,8 +1,8 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Min; fn k5_btsp() -> BottleneckTravelingSalesman { BottleneckTravelingSalesman::new( @@ -63,11 +63,11 @@ fn test_bottleneck_traveling_salesman_evaluate_valid_and_invalid() { let valid_cycle = vec![0, 1, 1, 0, 1, 0, 1, 0, 0, 1]; assert!(problem.is_valid_solution(&valid_cycle)); - assert_eq!(problem.evaluate(&valid_cycle), SolutionSize::Valid(4)); + assert_eq!(problem.evaluate(&valid_cycle), Min(Some(4))); let degree_violation = vec![1, 1, 1, 0, 1, 0, 1, 0, 0, 1]; assert!(!problem.is_valid_solution(°ree_violation)); - assert_eq!(problem.evaluate(°ree_violation), SolutionSize::Invalid); + assert_eq!(problem.evaluate(°ree_violation), Min(None)); } #[test] @@ -79,30 +79,17 @@ fn test_bottleneck_traveling_salesman_evaluate_disconnected_subtour_invalid() { let disconnected_subtour = vec![1, 1, 1, 1, 1, 1]; assert!(!problem.is_valid_solution(&disconnected_subtour)); - assert_eq!( - problem.evaluate(&disconnected_subtour), - SolutionSize::Invalid - ); -} - -#[test] -fn test_bottleneck_traveling_salesman_direction() { - let problem = BottleneckTravelingSalesman::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), - vec![7, 4, 6], - ); - - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.evaluate(&disconnected_subtour), Min(None)); } #[test] fn test_bottleneck_traveling_salesman_bruteforce_unique_optimum() { let problem = k5_btsp(); let solver = BruteForce::new(); - let best = solver.find_all_best(&problem); + let best = solver.find_all_witnesses(&problem); assert_eq!(best, vec![vec![0, 1, 1, 0, 1, 0, 1, 0, 0, 1]]); - assert_eq!(problem.evaluate(&best[0]), SolutionSize::Valid(4)); + assert_eq!(problem.evaluate(&best[0]), Min(Some(4))); } #[test] @@ -116,7 +103,7 @@ fn test_bottleneck_traveling_salesman_serialization() { assert_eq!(restored.weights(), problem.weights()); assert_eq!( restored.evaluate(&[0, 1, 1, 0, 1, 0, 1, 0, 0, 1]), - SolutionSize::Valid(4) + Min(Some(4)) ); } @@ -126,10 +113,10 @@ fn test_bottleneck_traveling_salesman_paper_example() { let config = vec![0, 1, 1, 0, 1, 0, 1, 0, 0, 1]; assert!(problem.is_valid_solution(&config)); - assert_eq!(problem.evaluate(&config), SolutionSize::Valid(4)); + assert_eq!(problem.evaluate(&config), Min(Some(4))); let solver = BruteForce::new(); - let best = solver.find_all_best(&problem); + let best = solver.find_all_witnesses(&problem); assert_eq!(best.len(), 1); assert_eq!(best[0], config); } diff --git a/src/unit_tests/models/graph/bounded_component_spanning_forest.rs b/src/unit_tests/models/graph/bounded_component_spanning_forest.rs index 2541b2e89..5e2573001 100644 --- a/src/unit_tests/models/graph/bounded_component_spanning_forest.rs +++ b/src/unit_tests/models/graph/bounded_component_spanning_forest.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; use std::alloc::{GlobalAlloc, Layout, System}; @@ -150,12 +150,12 @@ fn test_bounded_component_spanning_forest_solver_yes_and_no_instances() { let solver = BruteForce::new(); let yes_problem = yes_instance(); - let solution = solver.find_satisfying(&yes_problem); + let solution = solver.find_witness(&yes_problem); assert!(solution.is_some()); assert!(yes_problem.evaluate(solution.as_ref().unwrap())); let no_problem = no_instance(); - assert!(solver.find_satisfying(&no_problem).is_none()); + assert!(solver.find_witness(&no_problem).is_none()); } #[test] @@ -165,7 +165,7 @@ fn test_bounded_component_spanning_forest_paper_example() { assert!(problem.evaluate(&config)); let solver = BruteForce::new(); - let all_solutions = solver.find_all_satisfying(&problem); + let all_solutions = solver.find_all_witnesses(&problem); assert!(all_solutions.iter().any(|solution| solution == &config)); } diff --git a/src/unit_tests/models/graph/directed_two_commodity_integral_flow.rs b/src/unit_tests/models/graph/directed_two_commodity_integral_flow.rs index 57ea7952a..435f74744 100644 --- a/src/unit_tests/models/graph/directed_two_commodity_integral_flow.rs +++ b/src/unit_tests/models/graph/directed_two_commodity_integral_flow.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -99,7 +99,7 @@ fn test_directed_two_commodity_integral_flow_negative_net_flow_at_sink_is_infeas fn test_directed_two_commodity_integral_flow_solver_yes() { let problem = yes_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let sol = solution.unwrap(); assert!(problem.evaluate(&sol)); @@ -109,7 +109,7 @@ fn test_directed_two_commodity_integral_flow_solver_yes() { fn test_directed_two_commodity_integral_flow_solver_no() { let problem = no_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -154,7 +154,7 @@ fn test_directed_two_commodity_integral_flow_paper_example() { assert!(problem.evaluate(&config)); // Find all satisfying solutions and verify count - let all_solutions = solver.find_all_satisfying(&problem); + let all_solutions = solver.find_all_witnesses(&problem); assert!(!all_solutions.is_empty()); // Each solution must evaluate to true @@ -193,5 +193,5 @@ fn test_directed_two_commodity_integral_flow_higher_capacity() { assert!(problem.evaluate(&config)); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_some()); + assert!(solver.find_witness(&problem).is_some()); } diff --git a/src/unit_tests/models/graph/disjoint_connecting_paths.rs b/src/unit_tests/models/graph/disjoint_connecting_paths.rs index 2602ac0c3..6c613bd07 100644 --- a/src/unit_tests/models/graph/disjoint_connecting_paths.rs +++ b/src/unit_tests/models/graph/disjoint_connecting_paths.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -63,7 +63,7 @@ fn test_disjoint_connecting_paths_yes_instance() { fn test_disjoint_connecting_paths_no_instance() { let problem = issue_no_problem(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -103,7 +103,7 @@ fn test_disjoint_connecting_paths_paper_example() { assert!(problem.evaluate(&config)); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } diff --git a/src/unit_tests/models/graph/generalized_hex.rs b/src/unit_tests/models/graph/generalized_hex.rs index 9087cd898..7b9799099 100644 --- a/src/unit_tests/models/graph/generalized_hex.rs +++ b/src/unit_tests/models/graph/generalized_hex.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -64,9 +64,9 @@ fn test_generalized_hex_detects_losing_position() { fn test_generalized_hex_solver_returns_empty_config_for_win() { let problem = winning_example(); let solver = BruteForce::new(); - assert_eq!(solver.find_satisfying(&problem), Some(vec![])); + assert_eq!(solver.find_witness(&problem), Some(vec![])); assert_eq!( - solver.find_all_satisfying(&problem), + solver.find_all_witnesses(&problem), Vec::>::from([vec![]]) ); } @@ -99,7 +99,7 @@ fn test_generalized_hex_issue_example_is_losing_under_optimal_play() { fn test_generalized_hex_paper_example() { let problem = winning_example(); assert!(problem.evaluate(&[])); - assert_eq!(BruteForce::new().find_satisfying(&problem), Some(vec![])); + assert_eq!(BruteForce::new().find_witness(&problem), Some(vec![])); } #[test] diff --git a/src/unit_tests/models/graph/graph_partitioning.rs b/src/unit_tests/models/graph/graph_partitioning.rs index e674432b6..fe844ed47 100644 --- a/src/unit_tests/models/graph/graph_partitioning.rs +++ b/src/unit_tests/models/graph/graph_partitioning.rs @@ -1,8 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::Problem; /// Issue example: 6 vertices, edges forming two triangles connected by 3 edges. /// Optimal partition A={0,1,2}, B={3,4,5}, cut=3. @@ -36,13 +35,7 @@ fn test_graphpartitioning_basic() { // Crossing edges: (1,3), (2,3), (2,4) => cut = 3 let config = vec![0, 0, 0, 1, 1, 1]; let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Valid(3)); -} - -#[test] -fn test_graphpartitioning_direction() { - let problem = issue_example(); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(result, Min(Some(3))); } #[test] @@ -62,15 +55,15 @@ fn test_graphpartitioning_serialization() { fn test_graphpartitioning_solver() { let problem = issue_example(); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); let size = problem.evaluate(&best); - assert_eq!(size, SolutionSize::Valid(3)); + assert_eq!(size, Min(Some(3))); // All optimal solutions should have cut = 3 - let all_best = solver.find_all_best(&problem); + let all_best = solver.find_all_witnesses(&problem); assert!(!all_best.is_empty()); for sol in &all_best { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(sol), Min(Some(3))); } } @@ -86,7 +79,7 @@ fn test_graphpartitioning_odd_vertices() { for c in 0..2 { assert_eq!( problem.evaluate(&[a, b, c]), - SolutionSize::Invalid, + Min(None), "Expected Invalid for odd n, config [{}, {}, {}]", a, b, @@ -104,20 +97,20 @@ fn test_graphpartitioning_unbalanced_invalid() { let problem = GraphPartitioning::new(graph); // All zeros: 0 ones, not balanced - assert_eq!(problem.evaluate(&[0, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 0, 0]), Min(None)); // All ones: 4 ones, not balanced - assert_eq!(problem.evaluate(&[1, 1, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1, 1]), Min(None)); // One vertex in partition 1: not balanced - assert_eq!(problem.evaluate(&[1, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 0, 0]), Min(None)); // Three vertices in partition 1: not balanced - assert_eq!(problem.evaluate(&[1, 1, 1, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1, 0]), Min(None)); // Two vertices in partition 1: balanced, should be Valid // 4-cycle edges: (0,1),(1,2),(2,3),(0,3). Config [1,1,0,0] cuts (1,2) and (0,3) => cut=2 - assert_eq!(problem.evaluate(&[1, 1, 0, 0]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[1, 1, 0, 0]), Min(Some(2))); } #[test] @@ -134,11 +127,11 @@ fn test_graphpartitioning_square_graph() { let problem = GraphPartitioning::new(graph); let solver = BruteForce::new(); - let all_best = solver.find_all_best(&problem); + let all_best = solver.find_all_witnesses(&problem); // Minimum bisection of a 4-cycle: cut = 2 for sol in &all_best { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(sol), Min(Some(2))); } } @@ -165,5 +158,5 @@ fn test_graphpartitioning_empty_graph() { let problem = GraphPartitioning::new(graph); let config = vec![0, 0, 1, 1]; - assert_eq!(problem.evaluate(&config), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&config), Min(Some(0))); } diff --git a/src/unit_tests/models/graph/hamiltonian_circuit.rs b/src/unit_tests/models/graph/hamiltonian_circuit.rs index 3fd70b413..42f6e8d79 100644 --- a/src/unit_tests/models/graph/hamiltonian_circuit.rs +++ b/src/unit_tests/models/graph/hamiltonian_circuit.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -65,7 +65,7 @@ fn test_hamiltonian_circuit_small_graphs() { let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = HamiltonianCircuit::new(graph); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // K3 has 6 directed Hamiltonian circuits: 3 rotations x 2 directions assert_eq!(solutions.len(), 6); } @@ -77,7 +77,7 @@ fn test_hamiltonian_circuit_complete_graph_k4() { let problem = HamiltonianCircuit::new(graph); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // K4 has 3 distinct undirected Hamiltonian circuits, each yielding // 4 rotations x 2 directions = 8 directed permutations => 24 total assert_eq!(solutions.len(), 24); @@ -93,8 +93,8 @@ fn test_hamiltonian_circuit_no_solution() { let problem = HamiltonianCircuit::new(graph); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_witness(&problem).is_none()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] @@ -104,7 +104,7 @@ fn test_hamiltonian_circuit_solver() { let problem = HamiltonianCircuit::new(graph); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // 4-cycle has 8 Hamiltonian circuits: 4 starting positions x 2 directions assert_eq!(solutions.len(), 8); diff --git a/src/unit_tests/models/graph/hamiltonian_path.rs b/src/unit_tests/models/graph/hamiltonian_path.rs index 493769055..55078a641 100644 --- a/src/unit_tests/models/graph/hamiltonian_path.rs +++ b/src/unit_tests/models/graph/hamiltonian_path.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; #[test] @@ -30,7 +30,7 @@ fn test_hamiltonian_path_no_solution() { vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], )); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!( solution.is_none(), "Graph with isolated vertices has no Hamiltonian path" @@ -45,11 +45,11 @@ fn test_hamiltonian_path_brute_force() { let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); // Path graph P4 has exactly 2 Hamiltonian paths: 0-1-2-3 and 3-2-1-0 assert_eq!(all.len(), 2); for sol in &all { @@ -87,7 +87,7 @@ fn test_hamiltonian_path_complete_graph() { vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], )); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); // K4 has 4! = 24 Hamiltonian paths (all permutations) assert_eq!(all.len(), 24); } @@ -151,7 +151,7 @@ fn test_hamiltonianpath_paper_example() { // Verify with brute force let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert!(!all.is_empty()); for sol in &all { assert!(problem.evaluate(sol)); @@ -166,6 +166,6 @@ fn test_single_vertex() { let problem = HamiltonianPath::new(SimpleGraph::new(1, vec![])); assert!(problem.evaluate(&[0])); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 1); } diff --git a/src/unit_tests/models/graph/integral_flow_bundles.rs b/src/unit_tests/models/graph/integral_flow_bundles.rs index 67a6595e5..0e95b3c24 100644 --- a/src/unit_tests/models/graph/integral_flow_bundles.rs +++ b/src/unit_tests/models/graph/integral_flow_bundles.rs @@ -74,7 +74,7 @@ fn test_integral_flow_bundles_rejects_bad_bundle_sum_or_conservation() { fn test_integral_flow_bundles_solver_and_paper_example() { let problem = yes_instance(); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert!(!all.is_empty()); assert!(all.contains(&satisfying_config())); assert!(problem.evaluate(&satisfying_config())); diff --git a/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs b/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs index e9cfb245e..2ce6a5e7d 100644 --- a/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs +++ b/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -83,7 +83,7 @@ fn test_integral_flow_homologous_arcs_wrong_config_length_is_invalid() { fn test_integral_flow_homologous_arcs_solver_yes() { let problem = yes_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } @@ -92,7 +92,7 @@ fn test_integral_flow_homologous_arcs_solver_yes() { fn test_integral_flow_homologous_arcs_solver_no() { let problem = no_instance(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -126,7 +126,7 @@ fn test_integral_flow_homologous_arcs_non_unit_capacity() { assert!(problem.evaluate(&[3, 3])); assert!(!problem.evaluate(&[2, 3])); // homologous violation let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 2); // [2,2] and [3,3] } @@ -138,7 +138,9 @@ fn test_integral_flow_homologous_arcs_paper_example() { assert!(problem.evaluate(&config)); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); - assert!(solutions.iter().all(|solution| problem.evaluate(solution))); + assert!(solutions + .iter() + .all(|solution| problem.evaluate(solution).0)); } diff --git a/src/unit_tests/models/graph/integral_flow_with_multipliers.rs b/src/unit_tests/models/graph/integral_flow_with_multipliers.rs index d2dc7a65a..a4afd16af 100644 --- a/src/unit_tests/models/graph/integral_flow_with_multipliers.rs +++ b/src/unit_tests/models/graph/integral_flow_with_multipliers.rs @@ -1,6 +1,6 @@ use super::*; use crate::registry::declared_size_fields; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; use crate::traits::Problem; use std::collections::HashSet; @@ -64,7 +64,7 @@ fn test_integral_flow_with_multipliers_evaluate_yes_instance() { #[test] fn test_integral_flow_with_multipliers_evaluate_no_instance() { let solver = BruteForce::new(); - assert!(solver.find_satisfying(&no_instance()).is_none()); + assert!(solver.find_witness(&no_instance()).is_none()); } #[test] @@ -103,7 +103,7 @@ fn test_integral_flow_with_multipliers_serialization_round_trip() { fn test_integral_flow_with_multipliers_solver_yes_instance() { let problem = yes_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem).unwrap(); + let solution = solver.find_witness(&problem).unwrap(); assert!(problem.evaluate(&solution)); } @@ -144,6 +144,6 @@ fn test_integral_flow_with_multipliers_paper_example() { assert_eq!([config[6], config[8], config[10]], [2, 4, 6]); assert_eq!(config[6] + config[8] + config[10], 12); - let all_solutions = solver.find_all_satisfying(&problem); + let all_solutions = solver.find_all_witnesses(&problem); assert!(all_solutions.iter().any(|solution| solution == &config)); } diff --git a/src/unit_tests/models/graph/isomorphic_spanning_tree.rs b/src/unit_tests/models/graph/isomorphic_spanning_tree.rs index 3a871f223..b16d36f62 100644 --- a/src/unit_tests/models/graph/isomorphic_spanning_tree.rs +++ b/src/unit_tests/models/graph/isomorphic_spanning_tree.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -77,12 +77,12 @@ fn test_isomorphicspanningtree_solver_yes() { let problem = IsomorphicSpanningTree::new(graph, tree); let solver = BruteForce::new(); - let sol = solver.find_satisfying(&problem); + let sol = solver.find_witness(&problem); assert!(sol.is_some()); assert!(problem.evaluate(&sol.unwrap())); // All satisfying solutions should be valid - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert!(!all.is_empty()); for s in &all { assert!(problem.evaluate(s)); @@ -97,10 +97,10 @@ fn test_isomorphicspanningtree_solver_no() { let problem = IsomorphicSpanningTree::new(graph, tree); let solver = BruteForce::new(); - let sol = solver.find_satisfying(&problem); + let sol = solver.find_witness(&problem); assert!(sol.is_none()); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert!(all.is_empty()); } @@ -164,7 +164,7 @@ fn test_isomorphicspanningtree_paper_example() { // All 4! = 24 permutations should work since K4 has every edge let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 24); } diff --git a/src/unit_tests/models/graph/kclique.rs b/src/unit_tests/models/graph/kclique.rs index 4343118bc..fc42e1909 100644 --- a/src/unit_tests/models/graph/kclique.rs +++ b/src/unit_tests/models/graph/kclique.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -52,8 +52,8 @@ fn test_kclique_solver_finds_unique_witness() { let problem = KClique::new(issue_graph(), 3); let solver = BruteForce::new(); - assert_eq!(solver.find_satisfying(&problem), Some(issue_witness())); - assert_eq!(solver.find_all_satisfying(&problem), vec![issue_witness()]); + assert_eq!(solver.find_witness(&problem), Some(issue_witness())); + assert_eq!(solver.find_all_witnesses(&problem), vec![issue_witness()]); } #[test] @@ -73,5 +73,5 @@ fn test_kclique_paper_example() { let solver = BruteForce::new(); assert!(problem.evaluate(&issue_witness())); - assert_eq!(solver.find_all_satisfying(&problem), vec![issue_witness()]); + assert_eq!(solver.find_all_witnesses(&problem), vec![issue_witness()]); } diff --git a/src/unit_tests/models/graph/kcoloring.rs b/src/unit_tests/models/graph/kcoloring.rs index a2ab6e79a..2185b0337 100644 --- a/src/unit_tests/models/graph/kcoloring.rs +++ b/src/unit_tests/models/graph/kcoloring.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::variant::{K1, K2, K3, K4}; include!("../../jl_helpers.rs"); @@ -45,7 +45,7 @@ fn test_brute_force_path() { let problem = KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // All solutions should be valid for sol in &solutions { assert!(problem.evaluate(sol)); @@ -60,7 +60,7 @@ fn test_brute_force_triangle() { let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); // All three vertices have different colors @@ -76,7 +76,7 @@ fn test_triangle_2_colors() { let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // No valid solutions assert!(solutions.is_empty()); } @@ -106,7 +106,7 @@ fn test_empty_graph() { let problem = KColoring::::new(SimpleGraph::new(3, vec![])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // Any coloring is valid when there are no edges assert!(!solutions.is_empty()); for sol in &solutions { @@ -125,7 +125,7 @@ fn test_complete_graph_k4() { )); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); } @@ -163,7 +163,7 @@ fn test_jl_parity_evaluation() { let problem = KColoring::::new(SimpleGraph::new(nv, edges)); for eval in instance["evaluations"].as_array().unwrap() { let config = jl_parse_config(&eval["config"]); - let result: bool = problem.evaluate(&config); + let result = problem.evaluate(&config).0; let jl_size = eval["size"].as_i64().unwrap() as usize; assert_eq!( result, @@ -172,7 +172,7 @@ fn test_jl_parity_evaluation() { config ); } - let all_sat = BruteForce::new().find_all_satisfying(&problem); + let all_sat = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_sat: HashSet> = all_sat.into_iter().collect(); assert_eq!(rust_sat, jl_best, "KColoring satisfying solutions mismatch"); @@ -209,5 +209,5 @@ fn test_kcoloring_paper_example() { let graph2 = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); let problem2 = KColoring::::new(graph2); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem2).is_none()); + assert!(solver.find_witness(&problem2).is_none()); } diff --git a/src/unit_tests/models/graph/kth_best_spanning_tree.rs b/src/unit_tests/models/graph/kth_best_spanning_tree.rs index 22c812154..1c3464260 100644 --- a/src/unit_tests/models/graph/kth_best_spanning_tree.rs +++ b/src/unit_tests/models/graph/kth_best_spanning_tree.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -91,9 +91,9 @@ fn test_kthbestspanningtree_solver_exhaustive() { let solver = BruteForce::new(); // Exactly 2 spanning trees have weight ≤ 4, so exactly 2! = 2 satisfying configs. - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 2); - assert!(all.iter().all(|config| problem.evaluate(config))); + assert!(all.iter().all(|config| problem.evaluate(config).0)); } #[test] @@ -101,8 +101,8 @@ fn test_kthbestspanningtree_solver_no_instance() { let problem = no_instance(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_witness(&problem).is_none()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] @@ -110,9 +110,9 @@ fn test_kthbestspanningtree_small_exhaustive_search() { let problem = small_yes_instance(); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 6); - assert!(all.iter().all(|config| problem.evaluate(config))); + assert!(all.iter().all(|config| problem.evaluate(config).0)); } #[test] diff --git a/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs b/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs index f6a891e4b..a83afd1bf 100644 --- a/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs +++ b/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -135,10 +135,10 @@ fn test_length_bounded_disjoint_paths_rejects_non_binary_entries() { fn test_length_bounded_disjoint_paths_solver_yes_and_no() { let yes_problem = sample_yes_problem(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&yes_problem).is_some()); + assert!(solver.find_witness(&yes_problem).is_some()); let no_problem = LengthBoundedDisjointPaths::new(sample_yes_graph(), 0, 6, 2, 2); - assert!(solver.find_satisfying(&no_problem).is_none()); + assert!(solver.find_witness(&no_problem).is_none()); } #[test] @@ -185,9 +185,9 @@ fn test_length_bounded_disjoint_paths_paper_example() { let config = encode_paths(7, &[&[0, 1, 6], &[0, 2, 3, 6]]); assert!(problem.evaluate(&config)); - let satisfying = BruteForce::new().find_all_satisfying(&problem); + let satisfying = BruteForce::new().find_all_witnesses(&problem); assert_eq!(satisfying.len(), 6); assert!(satisfying .iter() - .all(|candidate| problem.evaluate(candidate))); + .all(|candidate| problem.evaluate(candidate).0)); } diff --git a/src/unit_tests/models/graph/longest_circuit.rs b/src/unit_tests/models/graph/longest_circuit.rs index c3407f801..b3c4e4798 100644 --- a/src/unit_tests/models/graph/longest_circuit.rs +++ b/src/unit_tests/models/graph/longest_circuit.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -72,7 +72,7 @@ fn test_longest_circuit_rejects_non_binary_and_below_bound_configs() { fn test_longest_circuit_bruteforce_yes_and_no() { let yes_problem = issue_problem(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&yes_problem).is_some()); + assert!(solver.find_witness(&yes_problem).is_some()); let no_problem = LongestCircuit::new( SimpleGraph::new( @@ -93,7 +93,7 @@ fn test_longest_circuit_bruteforce_yes_and_no() { vec![3, 2, 4, 1, 5, 2, 3, 2, 1, 2], 19, ); - assert!(solver.find_satisfying(&no_problem).is_none()); + assert!(solver.find_witness(&no_problem).is_none()); } #[test] @@ -113,7 +113,7 @@ fn test_longest_circuit_paper_example() { let config = vec![1, 1, 1, 1, 1, 1, 0, 0, 0, 0]; assert!(problem.evaluate(&config)); - let all = BruteForce::new().find_all_satisfying(&problem); + let all = BruteForce::new().find_all_witnesses(&problem); assert!(all.contains(&config)); } diff --git a/src/unit_tests/models/graph/longest_path.rs b/src/unit_tests/models/graph/longest_path.rs index 9b2d54ab0..9b61616fe 100644 --- a/src/unit_tests/models/graph/longest_path.rs +++ b/src/unit_tests/models/graph/longest_path.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, One, SolutionSize}; +use crate::traits::Problem; +use crate::types::{Max, One}; fn issue_problem() -> LongestPath { LongestPath::new( @@ -48,7 +48,6 @@ fn test_longest_path_creation() { assert_eq!(problem.dims(), vec![2; 10]); assert_eq!(problem.edge_lengths(), &[3, 2, 4, 1, 5, 2, 3, 2, 4, 1]); assert!(problem.is_weighted()); - assert_eq!(problem.direction(), Direction::Maximize); problem.set_lengths(vec![1; 10]); assert_eq!(problem.edge_lengths(), &[1; 10]); @@ -61,27 +60,15 @@ fn test_longest_path_creation() { fn test_longest_path_evaluate_valid_and_invalid_configs() { let problem = issue_problem(); - assert_eq!(problem.evaluate(&optimal_config()), SolutionSize::Valid(20)); - assert_eq!( - problem.evaluate(&suboptimal_config()), - SolutionSize::Valid(17) - ); + assert_eq!(problem.evaluate(&optimal_config()), Max(Some(20))); + assert_eq!(problem.evaluate(&suboptimal_config()), Max(Some(17))); assert!(problem.is_valid_solution(&optimal_config())); assert!(problem.is_valid_solution(&suboptimal_config())); - assert_eq!( - problem.evaluate(&[1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), - SolutionSize::Invalid - ); - assert_eq!( - problem.evaluate(&[1, 0, 1, 0, 1, 0, 0, 0, 0, 1]), - SolutionSize::Invalid - ); - assert_eq!( - problem.evaluate(&[1, 0, 1, 1, 1, 1, 1, 1, 1, 1]), - SolutionSize::Invalid - ); - assert_eq!(problem.evaluate(&[0; 10]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), Max(None)); + assert_eq!(problem.evaluate(&[1, 0, 1, 0, 1, 0, 0, 0, 0, 1]), Max(None)); + assert_eq!(problem.evaluate(&[1, 0, 1, 1, 1, 1, 1, 1, 1, 1]), Max(None)); + assert_eq!(problem.evaluate(&[0; 10]), Max(None)); assert!(!problem.is_valid_solution(&[1, 0, 1])); assert!(!problem.is_valid_solution(&[1, 0, 1, 0, 1, 0, 1, 0, 1, 2])); } @@ -91,11 +78,11 @@ fn test_longest_path_bruteforce_finds_issue_optimum() { let problem = issue_problem(); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(best, optimal_config()); - assert_eq!(problem.evaluate(&best), SolutionSize::Valid(20)); + assert_eq!(problem.evaluate(&best), Max(Some(20))); - let all_best = solver.find_all_best(&problem); + let all_best = solver.find_all_witnesses(&problem); assert_eq!(all_best, vec![optimal_config()]); } @@ -110,10 +97,7 @@ fn test_longest_path_serialization() { assert_eq!(restored.source_vertex(), 0); assert_eq!(restored.target_vertex(), 6); assert_eq!(restored.edge_lengths(), &[3, 2, 4, 1, 5, 2, 3, 2, 4, 1]); - assert_eq!( - restored.evaluate(&optimal_config()), - SolutionSize::Valid(20) - ); + assert_eq!(restored.evaluate(&optimal_config()), Max(Some(20))); } #[test] @@ -121,11 +105,11 @@ fn test_longest_path_source_equals_target_only_allows_empty_path() { let problem = LongestPath::new(SimpleGraph::path(3), vec![5, 7], 1, 1); assert!(problem.is_valid_solution(&[0, 0])); - assert_eq!(problem.evaluate(&[0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0, 0]), Max(Some(0))); assert!(!problem.is_valid_solution(&[1, 0])); - assert_eq!(problem.evaluate(&[1, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0]), Max(None)); - let best = BruteForce::new().find_best(&problem).unwrap(); + let best = BruteForce::new().find_witness(&problem).unwrap(); assert_eq!(best, vec![0, 0]); } @@ -133,15 +117,9 @@ fn test_longest_path_source_equals_target_only_allows_empty_path() { fn test_longestpath_paper_example() { let problem = issue_problem(); - assert_eq!(problem.evaluate(&optimal_config()), SolutionSize::Valid(20)); - assert_eq!( - problem.evaluate(&suboptimal_config()), - SolutionSize::Valid(17) - ); - assert_eq!( - problem.evaluate(&[1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), - SolutionSize::Invalid - ); + assert_eq!(problem.evaluate(&optimal_config()), Max(Some(20))); + assert_eq!(problem.evaluate(&suboptimal_config()), Max(Some(17))); + assert_eq!(problem.evaluate(&[1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), Max(None)); } #[test] diff --git a/src/unit_tests/models/graph/max_cut.rs b/src/unit_tests/models/graph/max_cut.rs index 345859e54..dcfadc6e6 100644 --- a/src/unit_tests/models/graph/max_cut.rs +++ b/src/unit_tests/models/graph/max_cut.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; include!("../../jl_helpers.rs"); @@ -53,15 +53,6 @@ fn test_edges() { assert_eq!(edges.len(), 2); } -#[test] -fn test_direction() { - use crate::traits::OptimizationProblem; - use crate::types::Direction; - - let problem = MaxCut::<_, i32>::unweighted(SimpleGraph::new(2, vec![(0, 1)])); - assert_eq!(problem.direction(), Direction::Maximize); -} - #[test] fn test_new() { let problem = MaxCut::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![5, 10]); @@ -122,7 +113,7 @@ fn test_jl_parity_evaluation() { config ); } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "MaxCut best solutions mismatch"); @@ -160,6 +151,6 @@ fn test_maxcut_paper_example() { assert_eq!(result.unwrap(), 5); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 5); } diff --git a/src/unit_tests/models/graph/maximal_is.rs b/src/unit_tests/models/graph/maximal_is.rs index be9cf494f..06f2b3566 100644 --- a/src/unit_tests/models/graph/maximal_is.rs +++ b/src/unit_tests/models/graph/maximal_is.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; include!("../../jl_helpers.rs"); @@ -66,15 +66,6 @@ fn test_is_maximal_independent_set_function() { assert!(!is_maximal_independent_set(&graph, &[true, true, false])); // Not independent } -#[test] -fn test_direction() { - use crate::traits::OptimizationProblem; - use crate::types::Direction; - - let problem = MaximalIS::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]); - assert_eq!(problem.direction(), Direction::Maximize); -} - #[test] fn test_weights() { let problem = MaximalIS::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); @@ -159,7 +150,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "MaximalIS best solutions mismatch"); @@ -207,6 +198,6 @@ fn test_maximal_is_paper_example() { // Verify optimal weight is 3 let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 3); } diff --git a/src/unit_tests/models/graph/maximum_clique.rs b/src/unit_tests/models/graph/maximum_clique.rs index 1004037a8..625d33b10 100644 --- a/src/unit_tests/models/graph/maximum_clique.rs +++ b/src/unit_tests/models/graph/maximum_clique.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::types::SolutionSize; +use crate::types::Max; #[test] fn test_clique_creation() { @@ -50,10 +50,10 @@ fn test_evaluate_valid() { ); // Valid: all three form a clique - assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(&[1, 1, 1]), Max(Some(3))); // Valid: any pair - assert_eq!(problem.evaluate(&[1, 1, 0]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[1, 1, 0]), Max(Some(2))); } #[test] @@ -64,10 +64,10 @@ fn test_evaluate_invalid() { let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); // Invalid: 0 and 2 are not adjacent - returns Invalid - assert_eq!(problem.evaluate(&[1, 0, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 1]), Max(None)); // Invalid: all three selected but not a clique - assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1]), Max(None)); } #[test] @@ -76,7 +76,7 @@ fn test_evaluate_empty() { let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); // Empty set is a valid clique with size 0 - assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0, 0, 0]), Max(Some(0))); } #[test] @@ -89,10 +89,10 @@ fn test_weighted_solution() { ); // Select vertex 2 (weight 30) - assert_eq!(problem.evaluate(&[0, 0, 1]), SolutionSize::Valid(30)); + assert_eq!(problem.evaluate(&[0, 0, 1]), Max(Some(30))); // Select all three (weights 10 + 20 + 30 = 60) - assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Valid(60)); + assert_eq!(problem.evaluate(&[1, 1, 1]), Max(Some(60))); } #[test] @@ -104,7 +104,7 @@ fn test_brute_force_triangle() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![1, 1, 1]); } @@ -117,7 +117,7 @@ fn test_brute_force_path() { let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Maximum size is 2 for sol in &solutions { let size: usize = sol.iter().sum(); @@ -135,11 +135,11 @@ fn test_brute_force_weighted() { let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 100, 1]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Should select {0, 1} (weight 101) or {1, 2} (weight 101) assert!(solutions.len() == 2); for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(101)); + assert_eq!(problem.evaluate(sol), Max(Some(101))); } } @@ -166,15 +166,6 @@ fn test_is_clique_function() { )); // Adjacent pair } -#[test] -fn test_direction() { - use crate::traits::OptimizationProblem; - use crate::types::Direction; - - let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); - assert_eq!(problem.direction(), Direction::Maximize); -} - #[test] fn test_edges() { let problem = MaximumClique::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), vec![1i32; 4]); @@ -188,7 +179,7 @@ fn test_empty_graph() { let problem = MaximumClique::new(SimpleGraph::new(3, vec![]), vec![1i32; 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 3); // Each solution should have exactly one vertex selected for sol in &solutions { @@ -206,7 +197,7 @@ fn test_is_clique_method() { assert!(problem.evaluate(&[1, 1, 0]).is_valid()); assert!(problem.evaluate(&[0, 1, 1]).is_valid()); // Invalid: 0-2 not adjacent - returns Invalid - assert_eq!(problem.evaluate(&[1, 0, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 1]), Max(None)); } #[test] @@ -247,15 +238,14 @@ fn test_complete_graph() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![1, 1, 1, 1]); // All vertices form a clique } #[test] fn test_clique_problem() { - use crate::traits::{OptimizationProblem, Problem}; - use crate::types::Direction; + use crate::traits::Problem; // Triangle graph: all pairs connected let p = MaximumClique::new( @@ -264,10 +254,9 @@ fn test_clique_problem() { ); assert_eq!(p.dims(), vec![2, 2, 2]); // Valid clique: select all 3 vertices (triangle is a clique) - assert_eq!(p.evaluate(&[1, 1, 1]), SolutionSize::Valid(3)); + assert_eq!(p.evaluate(&[1, 1, 1]), Max(Some(3))); // Valid clique: select just vertex 0 - assert_eq!(p.evaluate(&[1, 0, 0]), SolutionSize::Valid(1)); - assert_eq!(p.direction(), Direction::Maximize); + assert_eq!(p.evaluate(&[1, 0, 0]), Max(Some(1))); } #[test] @@ -304,6 +293,6 @@ fn test_clique_paper_example() { assert_eq!(result.unwrap(), 3); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 3); } diff --git a/src/unit_tests/models/graph/maximum_independent_set.rs b/src/unit_tests/models/graph/maximum_independent_set.rs index 5c0f2352d..4362cfc39 100644 --- a/src/unit_tests/models/graph/maximum_independent_set.rs +++ b/src/unit_tests/models/graph/maximum_independent_set.rs @@ -1,8 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::Problem; include!("../../jl_helpers.rs"); #[test] @@ -64,12 +63,6 @@ fn test_is_independent_set_function() { )); } -#[test] -fn test_direction() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); - assert_eq!(problem.direction(), Direction::Maximize); -} - #[test] fn test_edges() { let problem = @@ -155,7 +148,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "IS best solutions mismatch"); @@ -215,6 +208,6 @@ fn test_mis_paper_example() { // Verify this is optimal let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 4); } diff --git a/src/unit_tests/models/graph/maximum_matching.rs b/src/unit_tests/models/graph/maximum_matching.rs index d74eea97f..17c170e8c 100644 --- a/src/unit_tests/models/graph/maximum_matching.rs +++ b/src/unit_tests/models/graph/maximum_matching.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Max; include!("../../jl_helpers.rs"); #[test] @@ -58,17 +58,11 @@ fn test_is_matching_function() { assert!(is_matching(&graph, &[false, false, false])); // Empty is valid } -#[test] -fn test_direction() { - let problem = MaximumMatching::<_, i32>::unit_weights(SimpleGraph::new(2, vec![(0, 1)])); - assert_eq!(problem.direction(), Direction::Maximize); -} - #[test] fn test_empty_graph() { let problem = MaximumMatching::<_, i32>::unit_weights(SimpleGraph::new(3, vec![])); // Empty matching is valid with size 0 - assert_eq!(Problem::evaluate(&problem, &[]), SolutionSize::Valid(0)); + assert_eq!(Problem::evaluate(&problem, &[]), Max(Some(0))); } #[test] @@ -82,7 +76,7 @@ fn test_edges() { fn test_empty_sets() { let problem = MaximumMatching::<_, i32>::unit_weights(SimpleGraph::new(2, vec![])); // Empty matching - assert_eq!(Problem::evaluate(&problem, &[]), SolutionSize::Valid(0)); + assert_eq!(Problem::evaluate(&problem, &[]), Max(Some(0))); } #[test] @@ -147,7 +141,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "Matching best solutions mismatch"); @@ -190,6 +184,6 @@ fn test_matching_paper_example() { assert_eq!(result.unwrap(), 2); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 2); } diff --git a/src/unit_tests/models/graph/min_max_multicenter.rs b/src/unit_tests/models/graph/min_max_multicenter.rs index de1f018d3..daa7aeb8a 100644 --- a/src/unit_tests/models/graph/min_max_multicenter.rs +++ b/src/unit_tests/models/graph/min_max_multicenter.rs @@ -90,7 +90,7 @@ fn test_minmaxmulticenter_solver() { let problem = example_instance(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // All solutions should evaluate to true assert!(!solutions.is_empty()); diff --git a/src/unit_tests/models/graph/minimum_cut_into_bounded_sets.rs b/src/unit_tests/models/graph/minimum_cut_into_bounded_sets.rs index 5ea0d9ea7..a538cde62 100644 --- a/src/unit_tests/models/graph/minimum_cut_into_bounded_sets.rs +++ b/src/unit_tests/models/graph/minimum_cut_into_bounded_sets.rs @@ -1,7 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::Problem; +use crate::types::Aggregate; /// Build the example instance from issue #228: /// 8 vertices, 12 edges, s=0, t=7, B=5 @@ -127,7 +128,7 @@ fn test_minimumcutintoboundedsets_serialization() { fn test_minimumcutintoboundedsets_solver_satisfying() { let problem = example_instance(6); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!( solution.is_some(), "Should find a satisfying partition for K=6" @@ -142,7 +143,7 @@ fn test_minimumcutintoboundedsets_solver_no_solution() { let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); let problem = MinimumCutIntoBoundedSets::new(graph, vec![1, 1, 1], 0, 3, 3, 0); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!( solution.is_none(), "Should find no satisfying partition for K=0" @@ -181,7 +182,7 @@ fn test_minimumcutintoboundedsets_all_satisfying() { let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); let problem = MinimumCutIntoBoundedSets::new(graph, vec![1, 1], 0, 2, 2, 1); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // Two valid partitions: {0,1}|{2} and {0}|{1,2} assert_eq!(solutions.len(), 2); for sol in &solutions { @@ -202,16 +203,14 @@ fn test_minimumcutintoboundedsets_solver_no_solution_issue_instance() { // Issue #228 NO instance: K=5 on the 8-vertex graph has no valid partition let problem = example_instance(5); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!( solution.is_none(), "Should find no satisfying partition for K=5 on the 8-vertex instance" ); } -// Verify SatisfactionProblem marker trait is implemented #[test] -fn test_minimumcutintoboundedsets_is_satisfaction_problem() { - fn assert_satisfaction() {} - assert_satisfaction::>(); +fn test_minimumcutintoboundedsets_supports_witnesses() { + assert!( as Problem>::Value::supports_witnesses()); } diff --git a/src/unit_tests/models/graph/minimum_dominating_set.rs b/src/unit_tests/models/graph/minimum_dominating_set.rs index b891105c3..a1665a701 100644 --- a/src/unit_tests/models/graph/minimum_dominating_set.rs +++ b/src/unit_tests/models/graph/minimum_dominating_set.rs @@ -1,8 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::Problem; include!("../../jl_helpers.rs"); #[test] @@ -60,19 +59,13 @@ fn test_is_dominating_set_function() { assert!(!is_dominating_set(&graph, &[false, false, false, false])); } -#[test] -fn test_direction() { - let problem = MinimumDominatingSet::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_isolated_vertex() { // Isolated vertex must be in dominating set let problem = MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Vertex 2 is isolated, must be selected for sol in &solutions { assert_eq!(sol[2], 1); @@ -154,7 +147,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "DS best solutions mismatch"); @@ -191,6 +184,6 @@ fn test_mds_paper_example() { assert_eq!(result.unwrap(), 2); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 2); } diff --git a/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs b/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs index 1d9415c9b..c402935ac 100644 --- a/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs +++ b/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Min; fn issue_graph() -> DirectedGraph { DirectedGraph::new(6, vec![(0, 2), (0, 3), (1, 3), (1, 4), (2, 5)]) @@ -48,8 +48,7 @@ fn test_minimum_dummy_activities_pert_rejects_cyclic_input() { fn test_minimum_dummy_activities_pert_issue_example() { let problem = issue_problem(); let config = config_for_merges(&problem, &[(0, 2), (1, 4), (2, 5)]); - assert_eq!(problem.direction(), Direction::Minimize); - assert_eq!(problem.evaluate(&config), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&config), Min(Some(2))); assert!(problem.is_valid_solution(&config)); } @@ -57,15 +56,15 @@ fn test_minimum_dummy_activities_pert_issue_example() { fn test_minimum_dummy_activities_pert_rejects_spurious_reachability() { let problem = issue_problem(); let config = config_for_merges(&problem, &[(0, 3), (1, 3)]); - assert_eq!(problem.evaluate(&config), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&config), Min(None)); assert!(!problem.is_valid_solution(&config)); } #[test] fn test_minimum_dummy_activities_pert_solver_finds_optimum_two() { let problem = issue_problem(); - let solution = BruteForce::new().find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(2)); + let solution = BruteForce::new().find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), Min(Some(2))); } #[test] @@ -83,15 +82,15 @@ fn test_minimum_dummy_activities_pert_transitive_arc_zero_dummies() { // satisfied, so the optimal dummy count is 0. let dag = DirectedGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); let problem = MinimumDummyActivitiesPert::new(dag); - let solution = BruteForce::new().find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(0)); + let solution = BruteForce::new().find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), Min(Some(0))); } #[test] fn test_minimum_dummy_activities_pert_paper_example() { let problem = issue_problem(); let config = config_for_merges(&problem, &[(0, 2), (1, 4), (2, 5)]); - assert_eq!(problem.evaluate(&config), SolutionSize::Valid(2)); - let solution = BruteForce::new().find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&config), Min(Some(2))); + let solution = BruteForce::new().find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), Min(Some(2))); } diff --git a/src/unit_tests/models/graph/minimum_feedback_arc_set.rs b/src/unit_tests/models/graph/minimum_feedback_arc_set.rs index 09dd95f75..1ede2865b 100644 --- a/src/unit_tests/models/graph/minimum_feedback_arc_set.rs +++ b/src/unit_tests/models/graph/minimum_feedback_arc_set.rs @@ -1,8 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::Problem; #[test] fn test_minimum_feedback_arc_set_creation() { @@ -28,13 +27,6 @@ fn test_minimum_feedback_arc_set_creation() { assert!(problem.dims().iter().all(|&d| d == 2)); } -#[test] -fn test_minimum_feedback_arc_set_direction() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i32; 3]); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_minimum_feedback_arc_set_evaluation_valid() { // Simple cycle: 0->1->2->0 @@ -91,7 +83,7 @@ fn test_minimum_feedback_arc_set_solver_simple_cycle() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackArcSet::new(graph, vec![1i32; 3]); - let solutions = BruteForce::new().find_all_best(&problem); + let solutions = BruteForce::new().find_all_witnesses(&problem); // Minimum FAS has size 1 (remove any one arc) for sol in &solutions { assert_eq!(sol.iter().sum::(), 1); @@ -119,7 +111,7 @@ fn test_minimum_feedback_arc_set_solver_issue_example() { ); let problem = MinimumFeedbackArcSet::new(graph, vec![1i32; 9]); - let solution = BruteForce::new().find_best(&problem).unwrap(); + let solution = BruteForce::new().find_witness(&problem).unwrap(); // The optimal FAS has size 2 let fas_size: usize = solution.iter().sum(); assert_eq!(fas_size, 2); @@ -136,7 +128,7 @@ fn test_minimum_feedback_arc_set_weighted() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackArcSet::new(graph, vec![10i32, 1, 1]); - let solution = BruteForce::new().find_best(&problem).unwrap(); + let solution = BruteForce::new().find_witness(&problem).unwrap(); let result = problem.evaluate(&solution); assert!(result.is_valid()); assert_eq!(result.unwrap(), 1); // should pick a cheap arc @@ -180,7 +172,7 @@ fn test_minimum_feedback_arc_set_two_disjoint_cycles() { let graph = DirectedGraph::new(4, vec![(0, 1), (1, 0), (2, 3), (3, 2)]); let problem = MinimumFeedbackArcSet::new(graph, vec![1i32; 4]); - let solution = BruteForce::new().find_best(&problem).unwrap(); + let solution = BruteForce::new().find_witness(&problem).unwrap(); // Need to remove at least one arc from each cycle -> size 2 assert_eq!(solution.iter().sum::(), 2); } diff --git a/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs b/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs index 353c585b6..00fd26274 100644 --- a/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs +++ b/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs @@ -1,9 +1,8 @@ use super::is_feedback_vertex_set; use crate::models::graph::MinimumFeedbackVertexSet; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::Problem; /// Build the 9-vertex, 15-arc example from the issue. /// @@ -59,13 +58,6 @@ fn test_minimum_feedback_vertex_set_basic() { ); } -#[test] -fn test_minimum_feedback_vertex_set_direction() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i32; 3]); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_minimum_feedback_vertex_set_serialization() { let graph = example_graph(); @@ -86,14 +78,14 @@ fn test_minimum_feedback_vertex_set_solver() { let problem = MinimumFeedbackVertexSet::new(graph, vec![1i32; 9]); let solver = BruteForce::new(); - let best = solver.find_best(&problem); + let best = solver.find_witness(&problem); assert!(best.is_some(), "Expected a solution to exist"); let best_config = best.unwrap(); let best_result = problem.evaluate(&best_config); assert!(best_result.is_valid()); assert_eq!(best_result.unwrap(), 3, "Expected optimal FVS size 3"); - let all_best = BruteForce::new().find_all_best(&problem); + let all_best = BruteForce::new().find_all_witnesses(&problem); assert_eq!(all_best.len(), 18, "Expected 18 optimal FVS solutions"); } @@ -208,6 +200,6 @@ fn test_minimum_feedback_vertex_set_paper_example() { // Verify optimal FVS weight is 1 let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 1); } diff --git a/src/unit_tests/models/graph/minimum_multiway_cut.rs b/src/unit_tests/models/graph/minimum_multiway_cut.rs index 85bd2baa7..9cbbe5511 100644 --- a/src/unit_tests/models/graph/minimum_multiway_cut.rs +++ b/src/unit_tests/models/graph/minimum_multiway_cut.rs @@ -1,8 +1,8 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Min; #[test] fn test_minimummultiwaycut_creation() { @@ -25,7 +25,7 @@ fn test_minimummultiwaycut_evaluate_valid() { // config: [1, 0, 0, 1, 1, 0] => weight 2 + 2 + 4 = 8 let config = vec![1, 0, 0, 1, 1, 0]; let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Valid(8)); + assert_eq!(result, Min(Some(8))); } #[test] @@ -36,14 +36,7 @@ fn test_minimummultiwaycut_evaluate_invalid() { // No edges cut: all terminals connected => invalid let config = vec![0, 0, 0, 0, 0, 0]; let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Invalid); -} - -#[test] -fn test_minimummultiwaycut_direction() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 1]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(result, Min(None)); } #[test] @@ -53,11 +46,11 @@ fn test_minimummultiwaycut_brute_force() { let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { let val = problem.evaluate(sol); - assert_eq!(val, SolutionSize::Valid(8)); + assert_eq!(val, Min(Some(8))); } // Verify the claimed optimal cut [1,0,0,1,1,0] is among solutions let claimed_optimal = vec![1, 0, 0, 1, 1, 0]; @@ -77,9 +70,9 @@ fn test_minimummultiwaycut_two_terminals() { let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![3i32, 5]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(sol), Min(Some(3))); } } @@ -89,7 +82,7 @@ fn test_minimummultiwaycut_all_edges_cut() { let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); let config = vec![1, 1, 1, 1, 1, 1]; let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Valid(2 + 3 + 1 + 2 + 4 + 5)); + assert_eq!(result, Min(Some(2 + 3 + 1 + 2 + 4 + 5))); } #[test] @@ -100,12 +93,12 @@ fn test_minimummultiwaycut_already_disconnected() { let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 1]); let config = vec![0, 0]; let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Valid(0)); + assert_eq!(result, Min(Some(0))); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(sol), Min(Some(0))); } } @@ -172,10 +165,10 @@ fn test_minimummultiwaycut_short_config_no_panic() { // Short config: only 2 of 6 edges specified, terminals remain connected let short_config = vec![1, 0]; let result = problem.evaluate(&short_config); - assert_eq!(result, SolutionSize::Invalid); + assert_eq!(result, Min(None)); // Empty config: no edges cut, all terminals connected let empty_config: Vec = vec![]; let result = problem.evaluate(&empty_config); - assert_eq!(result, SolutionSize::Invalid); + assert_eq!(result, Min(None)); } diff --git a/src/unit_tests/models/graph/minimum_sum_multicenter.rs b/src/unit_tests/models/graph/minimum_sum_multicenter.rs index b6aa0aac0..fdf833111 100644 --- a/src/unit_tests/models/graph/minimum_sum_multicenter.rs +++ b/src/unit_tests/models/graph/minimum_sum_multicenter.rs @@ -1,8 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::Problem; #[test] fn test_min_sum_multicenter_creation() { @@ -24,13 +23,6 @@ fn test_min_sum_multicenter_size_getters() { assert_eq!(problem.num_centers(), 2); } -#[test] -fn test_min_sum_multicenter_direction() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_min_sum_multicenter_evaluate_path() { // Path: 0-1-2, unit weights and lengths, K=1 @@ -127,7 +119,7 @@ fn test_min_sum_multicenter_solver() { let problem = MinimumSumMulticenter::new(graph, vec![1i32; 7], vec![1i32; 8], 2); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); let best_cost = problem.evaluate(&best).unwrap(); // Optimal cost should be 6 (centers at {2, 5}) @@ -226,7 +218,7 @@ fn test_min_sum_multicenter_paper_example() { // Verify optimality with brute force let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 6); } @@ -238,13 +230,13 @@ fn test_min_sum_multicenter_dims() { } #[test] -fn test_min_sum_multicenter_find_all_best() { +fn test_min_sum_multicenter_find_all_witnesses() { // Path: 0-1-2, unit weights, K=1. Center at 1 is optimal (cost 2) let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); let problem = MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![0, 1, 0]); } diff --git a/src/unit_tests/models/graph/minimum_vertex_cover.rs b/src/unit_tests/models/graph/minimum_vertex_cover.rs index ba2a36648..39f052644 100644 --- a/src/unit_tests/models/graph/minimum_vertex_cover.rs +++ b/src/unit_tests/models/graph/minimum_vertex_cover.rs @@ -1,8 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::Problem; include!("../../jl_helpers.rs"); #[test] @@ -42,12 +41,6 @@ fn test_is_vertex_cover_function() { )); } -#[test] -fn test_direction() { - let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_complement_relationship() { // For a graph, if S is an independent set, then V\S is a vertex cover @@ -59,7 +52,7 @@ fn test_complement_relationship() { let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(&is_problem); + let is_solutions = solver.find_all_witnesses(&is_problem); for is_sol in &is_solutions { // Complement should be a valid vertex cover let vc_config: Vec = is_sol.iter().map(|&x| 1 - x).collect(); @@ -138,7 +131,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "VC best solutions mismatch"); @@ -173,6 +166,6 @@ fn test_mvc_paper_example() { assert_eq!(result.unwrap(), 3); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 3); } diff --git a/src/unit_tests/models/graph/mixed_chinese_postman.rs b/src/unit_tests/models/graph/mixed_chinese_postman.rs index e73da1ff6..74da8ae96 100644 --- a/src/unit_tests/models/graph/mixed_chinese_postman.rs +++ b/src/unit_tests/models/graph/mixed_chinese_postman.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::MixedGraph; use crate::traits::Problem; @@ -68,7 +68,7 @@ fn test_mixed_chinese_postman_single_edge_walk() { assert!(problem.evaluate(&[1])); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_some()); + assert!(solver.find_witness(&problem).is_some()); } #[test] @@ -102,7 +102,7 @@ fn test_mixed_chinese_postman_solver_finds_satisfying_orientation() { let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("expected a satisfying orientation"); assert!(problem.evaluate(&solution)); } @@ -112,7 +112,7 @@ fn test_mixed_chinese_postman_solver_reports_unsat_issue_example() { let problem = no_instance(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] diff --git a/src/unit_tests/models/graph/multiple_choice_branching.rs b/src/unit_tests/models/graph/multiple_choice_branching.rs index 9d31d30ed..80ceb6750 100644 --- a/src/unit_tests/models/graph/multiple_choice_branching.rs +++ b/src/unit_tests/models/graph/multiple_choice_branching.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; use crate::traits::Problem; use serde_json; @@ -165,11 +165,11 @@ fn test_multiple_choice_branching_solver_issue_examples() { let yes_problem = yes_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&yes_problem); + let solution = solver.find_witness(&yes_problem); assert!(solution.is_some()); assert!(yes_problem.evaluate(&solution.unwrap())); - let all_solutions = solver.find_all_satisfying(&yes_problem); + let all_solutions = solver.find_all_witnesses(&yes_problem); assert!(!all_solutions.is_empty()); assert!(all_solutions.contains(&vec![1, 0, 1, 0, 0, 1, 0, 1])); for config in &all_solutions { @@ -177,7 +177,7 @@ fn test_multiple_choice_branching_solver_issue_examples() { } let no_problem = no_instance(); - assert!(solver.find_satisfying(&no_problem).is_none()); + assert!(solver.find_witness(&no_problem).is_none()); } #[test] @@ -187,7 +187,7 @@ fn test_multiple_choice_branching_paper_example() { assert!(problem.evaluate(&config)); - let all_solutions = BruteForce::new().find_all_satisfying(&problem); + let all_solutions = BruteForce::new().find_all_witnesses(&problem); assert_eq!(all_solutions.len(), 11); assert!(all_solutions.contains(&config)); } diff --git a/src/unit_tests/models/graph/multiple_copy_file_allocation.rs b/src/unit_tests/models/graph/multiple_copy_file_allocation.rs index 56a9c7b7d..4f358ff71 100644 --- a/src/unit_tests/models/graph/multiple_copy_file_allocation.rs +++ b/src/unit_tests/models/graph/multiple_copy_file_allocation.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -94,9 +94,9 @@ fn test_multiple_copy_file_allocation_solver_yes_and_no() { let no_problem = cycle_no_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&yes_problem).unwrap(); + let solution = solver.find_witness(&yes_problem).unwrap(); assert!(yes_problem.evaluate(&solution)); - assert!(solver.find_satisfying(&no_problem).is_none()); + assert!(solver.find_witness(&no_problem).is_none()); } #[test] @@ -121,7 +121,7 @@ fn test_multiple_copy_file_allocation_paper_example() { assert_eq!(problem.total_cost(&config), Some(33)); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 36); assert!(all.iter().any(|candidate| candidate == &config)); } diff --git a/src/unit_tests/models/graph/optimal_linear_arrangement.rs b/src/unit_tests/models/graph/optimal_linear_arrangement.rs index c8371791a..94e1e025b 100644 --- a/src/unit_tests/models/graph/optimal_linear_arrangement.rs +++ b/src/unit_tests/models/graph/optimal_linear_arrangement.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -52,7 +52,7 @@ fn test_optimallineararrangement_no_instance() { // Brute-force confirms no arrangement achieves cost <= 9 let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -104,13 +104,13 @@ fn test_optimallineararrangement_solver() { let problem = OptimalLinearArrangement::new(graph, 4); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let sol = solution.unwrap(); assert!(problem.evaluate(&sol)); // All satisfying solutions should be valid - let all_sat = solver.find_all_satisfying(&problem); + let all_sat = solver.find_all_witnesses(&problem); assert!(!all_sat.is_empty()); for s in &all_sat { assert!(problem.evaluate(s)); @@ -125,10 +125,10 @@ fn test_optimallineararrangement_solver_no_solution() { let problem = OptimalLinearArrangement::new(graph, 3); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); - let all_sat = solver.find_all_satisfying(&problem); + let all_sat = solver.find_all_witnesses(&problem); assert!(all_sat.is_empty()); } @@ -139,7 +139,7 @@ fn test_optimallineararrangement_empty_graph() { let problem = OptimalLinearArrangement::new(graph, 0); let solver = BruteForce::new(); - let all_sat = solver.find_all_satisfying(&problem); + let all_sat = solver.find_all_witnesses(&problem); // All 3! = 6 permutations should be valid assert_eq!(all_sat.len(), 6); for s in &all_sat { @@ -241,7 +241,7 @@ fn test_optimallineararrangement_complete_graph_k4() { let problem = OptimalLinearArrangement::new(graph, 10); let solver = BruteForce::new(); - let all_sat = solver.find_all_satisfying(&problem); + let all_sat = solver.find_all_witnesses(&problem); // All 4! = 24 permutations should be valid since all have cost 10 assert_eq!(all_sat.len(), 24); for sol in &all_sat { diff --git a/src/unit_tests/models/graph/partial_feedback_edge_set.rs b/src/unit_tests/models/graph/partial_feedback_edge_set.rs index b721095f6..2f6974355 100644 --- a/src/unit_tests/models/graph/partial_feedback_edge_set.rs +++ b/src/unit_tests/models/graph/partial_feedback_edge_set.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; @@ -95,11 +95,11 @@ fn test_partial_feedback_edge_set_solver_yes_and_no_instances() { let solver = BruteForce::new(); let yes_problem = yes_instance(); - let solution = solver.find_satisfying(&yes_problem).unwrap(); + let solution = solver.find_witness(&yes_problem).unwrap(); assert!(yes_problem.evaluate(&solution)); let no_problem = no_instance(); - assert!(solver.find_satisfying(&no_problem).is_none()); + assert!(solver.find_witness(&no_problem).is_none()); } #[test] @@ -108,7 +108,7 @@ fn test_partial_feedback_edge_set_paper_example() { let config = select_edges(problem.graph(), &[(0, 2), (2, 3), (3, 4)]); assert!(problem.evaluate(&config)); - let satisfying = BruteForce::new().find_all_satisfying(&problem); + let satisfying = BruteForce::new().find_all_witnesses(&problem); assert_eq!(satisfying.len(), 5); assert!(satisfying.iter().any(|candidate| candidate == &config)); } diff --git a/src/unit_tests/models/graph/partition_into_paths_of_length_2.rs b/src/unit_tests/models/graph/partition_into_paths_of_length_2.rs index c576cdef4..3df87c0bd 100644 --- a/src/unit_tests/models/graph/partition_into_paths_of_length_2.rs +++ b/src/unit_tests/models/graph/partition_into_paths_of_length_2.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -51,7 +51,7 @@ fn test_partition_into_paths_no_solution() { assert_eq!(problem.num_groups(), 2); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none(), "Expected no solution for this graph"); } @@ -62,7 +62,7 @@ fn test_partition_into_paths_solver() { let problem = PartitionIntoPathsOfLength2::new(graph); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty(), "Expected at least one solution"); for sol in &solutions { diff --git a/src/unit_tests/models/graph/partition_into_triangles.rs b/src/unit_tests/models/graph/partition_into_triangles.rs index 47b777e22..f1ad4d4f8 100644 --- a/src/unit_tests/models/graph/partition_into_triangles.rs +++ b/src/unit_tests/models/graph/partition_into_triangles.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; #[test] @@ -51,7 +51,7 @@ fn test_partitionintotriangles_no_solution() { // No valid partition exists since there are no triangles let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -64,13 +64,13 @@ fn test_partitionintotriangles_solver() { let problem = PartitionIntoTriangles::new(graph); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let sol = solution.unwrap(); assert!(problem.evaluate(&sol)); // All solutions should be valid - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert!(!all.is_empty()); for s in &all { assert!(problem.evaluate(s)); @@ -139,6 +139,6 @@ fn test_partitionintotriangles_paper_example() { assert!(problem.evaluate(&[0, 0, 0, 1, 1, 1])); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); } diff --git a/src/unit_tests/models/graph/path_constrained_network_flow.rs b/src/unit_tests/models/graph/path_constrained_network_flow.rs index 0fb6625b7..751cde06e 100644 --- a/src/unit_tests/models/graph/path_constrained_network_flow.rs +++ b/src/unit_tests/models/graph/path_constrained_network_flow.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -84,11 +84,11 @@ fn test_path_constrained_network_flow_solver_yes_and_no() { let no = no_instance(); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&yes); + let satisfying = solver.find_all_witnesses(&yes); assert_eq!(satisfying.len(), 2); - assert!(satisfying.iter().all(|config| yes.evaluate(config))); + assert!(satisfying.iter().all(|config| yes.evaluate(config).0)); - assert!(solver.find_satisfying(&no).is_none()); + assert!(solver.find_witness(&no).is_none()); } #[test] @@ -148,7 +148,7 @@ fn test_path_constrained_network_flow_paper_example() { assert!(problem.evaluate(&config)); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 2); assert!(all.contains(&config)); } diff --git a/src/unit_tests/models/graph/rooted_tree_arrangement.rs b/src/unit_tests/models/graph/rooted_tree_arrangement.rs index 2e162c649..97040a1c3 100644 --- a/src/unit_tests/models/graph/rooted_tree_arrangement.rs +++ b/src/unit_tests/models/graph/rooted_tree_arrangement.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -86,7 +86,7 @@ fn test_rootedtreearrangement_solver_and_serialization() { let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("expected satisfying solution"); assert!(problem.evaluate(&solution)); diff --git a/src/unit_tests/models/graph/rural_postman.rs b/src/unit_tests/models/graph/rural_postman.rs index 9e72648bb..bc872b669 100644 --- a/src/unit_tests/models/graph/rural_postman.rs +++ b/src/unit_tests/models/graph/rural_postman.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -143,7 +143,7 @@ fn test_rural_postman_disconnected_selection() { fn test_rural_postman_brute_force_finds_solution() { let problem = chinese_postman_rpp(); let solver = BruteForce::new(); - let result = solver.find_satisfying(&problem); + let result = solver.find_witness(&problem); assert!(result.is_some()); let sol = result.unwrap(); assert!(problem.evaluate(&sol)); @@ -153,7 +153,7 @@ fn test_rural_postman_brute_force_finds_solution() { fn test_rural_postman_brute_force_hexagon() { let problem = hexagon_rpp(); let solver = BruteForce::new(); - let result = solver.find_satisfying(&problem); + let result = solver.find_witness(&problem); assert!(result.is_some()); let sol = result.unwrap(); assert!(problem.evaluate(&sol)); @@ -171,18 +171,18 @@ fn test_rural_postman_brute_force_no_solution() { let bound = 4; let problem = RuralPostman::new(graph, edge_lengths, required_edges, bound); let solver = BruteForce::new(); - let result = solver.find_satisfying(&problem); + let result = solver.find_witness(&problem); assert!(result.is_none()); } #[test] -fn test_rural_postman_find_all_satisfying() { +fn test_rural_postman_find_all_witnesses() { // Issue #248 instance 1: hexagonal graph, 6 vertices, 8 edges // Required edges E'={{0,1},{2,3},{4,5}}, B=6 // Search space = 3^8 = 6561 let problem = hexagon_rpp(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); } @@ -193,7 +193,7 @@ fn test_rural_postman_find_all_satisfying() { } #[test] -fn test_rural_postman_find_all_satisfying_empty() { +fn test_rural_postman_find_all_witnesses_empty() { // Issue #248 instance 2: required edges {0,1} and {4,5} are far apart // Minimum circuit cost ≥ 8 > B=4 let graph = SimpleGraph::new( @@ -205,7 +205,7 @@ fn test_rural_postman_find_all_satisfying_empty() { let bound = 4; let problem = RuralPostman::new(graph, edge_lengths, required_edges, bound); let solver = BruteForce::new(); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] diff --git a/src/unit_tests/models/graph/shortest_weight_constrained_path.rs b/src/unit_tests/models/graph/shortest_weight_constrained_path.rs index 0e03b95b9..18f1dbc89 100644 --- a/src/unit_tests/models/graph/shortest_weight_constrained_path.rs +++ b/src/unit_tests/models/graph/shortest_weight_constrained_path.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -65,11 +65,11 @@ fn test_shortest_weight_constrained_path_accessors() { fn test_shortest_weight_constrained_path_bruteforce() { let problem = issue_problem(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 2); for config in &all { assert!(problem.evaluate(config)); @@ -100,7 +100,7 @@ fn test_shortest_weight_constrained_path_no_solution() { 4, ); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -130,7 +130,7 @@ fn test_shortestweightconstrainedpath_paper_example() { let problem = issue_problem(); assert!(problem.evaluate(&[0, 1, 0, 1, 0, 1, 0, 0])); - let all = BruteForce::new().find_all_satisfying(&problem); + let all = BruteForce::new().find_all_witnesses(&problem); assert_eq!(all.len(), 2); } diff --git a/src/unit_tests/models/graph/spin_glass.rs b/src/unit_tests/models/graph/spin_glass.rs index ae05d127f..e5ff4fdea 100644 --- a/src/unit_tests/models/graph/spin_glass.rs +++ b/src/unit_tests/models/graph/spin_glass.rs @@ -1,7 +1,6 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::Problem; include!("../../jl_helpers.rs"); #[test] @@ -67,12 +66,6 @@ fn test_compute_energy_with_fields() { assert_eq!(problem.compute_energy(&[-1, 1]), -2.0); // -1 - 1 = -2 } -#[test] -fn test_direction() { - let problem = SpinGlass::::without_fields(2, vec![]); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_num_variables() { let problem = SpinGlass::::without_fields(5, vec![]); @@ -130,7 +123,7 @@ fn test_jl_parity_evaluation() { config ); } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_flip_configs_set(&jl_parse_configs_set(&instance["best_solutions"])); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "SpinGlass best solutions mismatch"); @@ -172,7 +165,7 @@ fn test_spinglass_paper_example() { assert_eq!(result.unwrap(), -3); // Verify this is optimal - let all_best = BruteForce::new().find_all_best(&problem); + let all_best = BruteForce::new().find_all_witnesses(&problem); assert!(!all_best.is_empty()); assert_eq!(problem.evaluate(&all_best[0]).unwrap(), -3); } diff --git a/src/unit_tests/models/graph/steiner_tree.rs b/src/unit_tests/models/graph/steiner_tree.rs index 1c9fc9026..00cd8d505 100644 --- a/src/unit_tests/models/graph/steiner_tree.rs +++ b/src/unit_tests/models/graph/steiner_tree.rs @@ -1,10 +1,5 @@ use super::*; -use crate::{ - solvers::BruteForce, - topology::SimpleGraph, - traits::{OptimizationProblem, Problem}, - types::Direction, -}; +use crate::{solvers::BruteForce, topology::SimpleGraph, traits::Problem}; /// Issue #122 example: 5 vertices, 7 edges, terminals {0, 2, 4}. /// Edges in order: (0,1)=2, (0,3)=5, (1,2)=2, (1,3)=1, (2,3)=5, (2,4)=6, (3,4)=1 @@ -34,12 +29,6 @@ fn test_steiner_tree_rejects_duplicate_terminals() { let _ = SteinerTree::new(graph, vec![1, 1], vec![0, 0]); } -#[test] -fn test_steiner_tree_direction() { - let problem = example_instance(); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_steiner_tree_size_getters() { let problem = example_instance(); @@ -54,7 +43,7 @@ fn test_steiner_tree_evaluate_optimal() { // Optimal: edges (0,1)=2, (1,2)=2, (1,3)=1, (3,4)=1 => cost 6 // Edge indices: 0=(0,1), 2=(1,2), 3=(1,3), 6=(3,4) let config = vec![1, 0, 1, 1, 0, 0, 1]; - assert_eq!(problem.evaluate(&config), SolutionSize::Valid(6)); + assert_eq!(problem.evaluate(&config), Min(Some(6))); } #[test] @@ -62,7 +51,7 @@ fn test_steiner_tree_evaluate_invalid_disconnected() { let problem = example_instance(); // Only edge (0,1) — terminals 2, 4 unreachable let config = vec![1, 0, 0, 0, 0, 0, 0]; - assert_eq!(problem.evaluate(&config), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&config), Min(None)); } #[test] @@ -70,25 +59,25 @@ fn test_steiner_tree_evaluate_invalid_cycle() { let problem = example_instance(); // Edges (0,1), (0,3), (1,2), (1,3), (3,4) — cycle 0-1-3-0 let config = vec![1, 1, 1, 1, 0, 0, 1]; - assert_eq!(problem.evaluate(&config), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&config), Min(None)); } #[test] fn test_steiner_tree_evaluate_empty() { let problem = example_instance(); let config = vec![0; 7]; - assert_eq!(problem.evaluate(&config), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&config), Min(None)); } #[test] fn test_steiner_tree_brute_force() { let problem = example_instance(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); // All optimal solutions should have cost 6 for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(6)); + assert_eq!(problem.evaluate(sol), Min(Some(6))); } } @@ -100,11 +89,11 @@ fn test_steiner_tree_all_terminals() { let terminals = vec![0, 1, 2]; let problem = SteinerTree::new(graph, edge_weights, terminals); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); // MST = edges (0,1)=1, (1,2)=2 => cost 3 for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(sol), Min(Some(3))); } } @@ -156,7 +145,7 @@ fn test_steiner_tree_disconnected_non_terminal_edges() { // Edges: 0=(0,1), 1=(1,2), 2=(2,3), 3=(3,4) // Select edges 0, 1, 3 — disconnected: {0,1,2} and {3,4} let config = vec![1, 1, 0, 1]; - assert_eq!(problem.evaluate(&config), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&config), Min(None)); assert!(!problem.is_valid_solution(&config)); } @@ -171,7 +160,7 @@ fn test_steiner_tree_edge_weights_and_set_weights() { assert_eq!(problem.edge_weights(), &[1, 1, 1, 1, 1, 1, 1]); // The same tree (0,1),(1,2),(1,3),(3,4) now costs 4 let config = vec![1, 0, 1, 1, 0, 0, 1]; - assert_eq!(problem.evaluate(&config), SolutionSize::Valid(4)); + assert_eq!(problem.evaluate(&config), Min(Some(4))); } #[test] diff --git a/src/unit_tests/models/graph/steiner_tree_in_graphs.rs b/src/unit_tests/models/graph/steiner_tree_in_graphs.rs index 1333589c2..cf09155cc 100644 --- a/src/unit_tests/models/graph/steiner_tree_in_graphs.rs +++ b/src/unit_tests/models/graph/steiner_tree_in_graphs.rs @@ -1,8 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::Problem; #[test] fn test_steiner_tree_creation() { @@ -47,13 +46,6 @@ fn test_steiner_tree_evaluation() { assert!(!result.is_valid()); } -#[test] -fn test_steiner_tree_direction() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![1i32; 2]); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_steiner_tree_solver() { // Diamond graph: @@ -69,7 +61,7 @@ fn test_steiner_tree_solver() { let problem = SteinerTreeInGraphs::new(graph, vec![0, 3], vec![2, 1, 2, 1]); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); + let solution = solver.find_witness(&problem).unwrap(); let value = problem.evaluate(&solution); assert!(value.is_valid()); assert_eq!(value.unwrap(), 2); @@ -87,7 +79,7 @@ fn test_steiner_tree_with_steiner_vertices() { let problem = SteinerTreeInGraphs::new(graph, vec![0, 2, 3], vec![1i32; 3]); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); + let solution = solver.find_witness(&problem).unwrap(); let value = problem.evaluate(&solution); assert!(value.is_valid()); assert_eq!(value.unwrap(), 3); @@ -158,7 +150,7 @@ fn test_steiner_tree_all_vertices_terminal() { let problem = SteinerTreeInGraphs::new(graph, vec![0, 1, 2], vec![1i32; 2]); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); + let solution = solver.find_witness(&problem).unwrap(); let value = problem.evaluate(&solution); assert!(value.is_valid()); assert_eq!(value.unwrap(), 2); @@ -212,7 +204,7 @@ fn test_steiner_tree_example_from_issue() { // Brute-force verification: independently confirm optimal weight is 12 let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); + let solution = solver.find_witness(&problem).unwrap(); let value = problem.evaluate(&solution); assert!(value.is_valid()); assert_eq!(value.unwrap(), 12); diff --git a/src/unit_tests/models/graph/strong_connectivity_augmentation.rs b/src/unit_tests/models/graph/strong_connectivity_augmentation.rs index 4231fffac..4577fb5ab 100644 --- a/src/unit_tests/models/graph/strong_connectivity_augmentation.rs +++ b/src/unit_tests/models/graph/strong_connectivity_augmentation.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -117,10 +117,10 @@ fn test_strong_connectivity_augmentation_solver() { let problem = issue_example_yes(); let solver = BruteForce::new(); - let satisfying = solver.find_satisfying(&problem).unwrap(); + let satisfying = solver.find_witness(&problem).unwrap(); assert!(problem.evaluate(&satisfying)); - let all_satisfying = solver.find_all_satisfying(&problem); + let all_satisfying = solver.find_all_witnesses(&problem); assert_eq!(all_satisfying, vec![yes_config()]); } diff --git a/src/unit_tests/models/graph/subgraph_isomorphism.rs b/src/unit_tests/models/graph/subgraph_isomorphism.rs index 8065db508..b8248cd3f 100644 --- a/src/unit_tests/models/graph/subgraph_isomorphism.rs +++ b/src/unit_tests/models/graph/subgraph_isomorphism.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -75,7 +75,7 @@ fn test_subgraph_isomorphism_no_solution() { // No possible mapping should work let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -88,7 +88,7 @@ fn test_subgraph_isomorphism_solver() { let problem = SubgraphIsomorphism::new(host, pattern); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let sol = solution.unwrap(); @@ -104,7 +104,7 @@ fn test_subgraph_isomorphism_all_satisfying() { let problem = SubgraphIsomorphism::new(host, pattern); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // 3 edges in host, each can be mapped in 2 directions = 6 solutions assert_eq!(solutions.len(), 6); for sol in &solutions { @@ -188,7 +188,7 @@ fn test_subgraph_isomorphism_issue_example() { // Verify solver can find a solution let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } diff --git a/src/unit_tests/models/graph/traveling_salesman.rs b/src/unit_tests/models/graph/traveling_salesman.rs index 256729509..1dc55c774 100644 --- a/src/unit_tests/models/graph/traveling_salesman.rs +++ b/src/unit_tests/models/graph/traveling_salesman.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::Min; fn k4_tsp() -> TravelingSalesman { TravelingSalesman::new( @@ -46,7 +46,7 @@ fn test_evaluate_valid_cycle() { vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], )); // Select all edges -> valid Hamiltonian cycle, cost = 5 - assert_eq!(problem.evaluate(&[1, 1, 1, 1, 1]), SolutionSize::Valid(5)); + assert_eq!(problem.evaluate(&[1, 1, 1, 1, 1]), Min(Some(5))); } #[test] @@ -55,7 +55,7 @@ fn test_evaluate_invalid_degree() { let problem = k4_tsp(); // edges: 0-1, 0-2, 0-3, 1-2, 1-3, 2-3 // Select first 3 edges (all incident to 0): degree(0)=3 -> Invalid - assert_eq!(problem.evaluate(&[1, 1, 1, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1, 0, 0, 0]), Min(None)); } #[test] @@ -66,7 +66,7 @@ fn test_evaluate_invalid_not_connected() { vec![(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5)], )); // Select all 6 edges: two disjoint cycles, not a single Hamiltonian cycle - assert_eq!(problem.evaluate(&[1, 1, 1, 1, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1, 1, 1, 1]), Min(None)); } #[test] @@ -76,7 +76,7 @@ fn test_evaluate_invalid_wrong_edge_count() { 5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], )); - assert_eq!(problem.evaluate(&[1, 1, 1, 1, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1, 1, 0]), Min(None)); } #[test] @@ -85,7 +85,7 @@ fn test_evaluate_no_edges_selected() { 5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], )); - assert_eq!(problem.evaluate(&[0, 0, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 0, 0, 0]), Min(None)); } #[test] @@ -93,11 +93,11 @@ fn test_brute_force_k4() { // Instance 1 from issue: K4 with weights let problem = k4_tsp(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); // Optimal cycle: 0->1->3->2->0, cost = 10+25+30+15 = 80 for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(80)); + assert_eq!(problem.evaluate(sol), Min(Some(80))); } } @@ -109,7 +109,7 @@ fn test_brute_force_path_graph_no_solution() { vec![(0, 1), (1, 2), (2, 3)], )); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.is_empty()); } @@ -121,10 +121,10 @@ fn test_brute_force_c5_unique_solution() { vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], )); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![1, 1, 1, 1, 1]); - assert_eq!(problem.evaluate(&solutions[0]), SolutionSize::Valid(5)); + assert_eq!(problem.evaluate(&solutions[0]), Min(Some(5))); } #[test] @@ -135,19 +135,10 @@ fn test_brute_force_bipartite_no_solution() { vec![(0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4)], )); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.is_empty()); } -#[test] -fn test_direction() { - let problem = TravelingSalesman::<_, i32>::unit_weights(SimpleGraph::new( - 3, - vec![(0, 1), (1, 2), (0, 2)], - )); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_problem_name() { assert_eq!( @@ -217,10 +208,10 @@ fn test_brute_force_triangle_weighted() { vec![5, 10, 15], ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![1, 1, 1]); - assert_eq!(problem.evaluate(&solutions[0]), SolutionSize::Valid(30)); + assert_eq!(problem.evaluate(&solutions[0]), Min(Some(30))); } #[test] @@ -258,9 +249,9 @@ fn test_tsp_paper_example() { // Tour uses edges 0, 2, 3, 5 let config = vec![1, 0, 1, 1, 0, 1]; let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Valid(6)); + assert_eq!(result, Min(Some(6))); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&best), SolutionSize::Valid(6)); + let best = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&best), Min(Some(6))); } diff --git a/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs b/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs index 9cb507b54..ab54df324 100644 --- a/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs +++ b/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; @@ -59,7 +59,7 @@ fn test_undirected_flow_lower_bounds_evaluation_yes() { fn test_undirected_flow_lower_bounds_evaluation_no() { let problem = canonical_no_instance(); assert!(!problem.evaluate(&[0, 0, 0, 0])); - assert!(BruteForce::new().find_satisfying(&problem).is_none()); + assert!(BruteForce::new().find_witness(&problem).is_none()); } #[test] @@ -87,7 +87,7 @@ fn test_undirected_flow_lower_bounds_serialization() { fn test_undirected_flow_lower_bounds_solver_yes() { let problem = canonical_yes_instance(); let solution = BruteForce::new() - .find_satisfying(&problem) + .find_witness(&problem) .expect("expected a satisfying orientation"); assert!(problem.evaluate(&solution)); assert_eq!(solution.len(), problem.num_edges()); @@ -99,6 +99,6 @@ fn test_undirected_flow_lower_bounds_paper_example() { let config = yes_orientation_config(); assert!(problem.evaluate(&config)); - let all = BruteForce::new().find_all_satisfying(&problem); + let all = BruteForce::new().find_all_witnesses(&problem); assert!(all.contains(&config)); } diff --git a/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs b/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs index e34f7d92a..c34f21bc9 100644 --- a/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs +++ b/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; @@ -66,7 +66,7 @@ fn test_undirected_two_commodity_integral_flow_evaluation_no_shared_bottleneck() let problem = shared_bottleneck_instance(); assert!(!problem.evaluate(&example_config())); assert!(!problem.is_valid_solution(&example_config())); - assert!(BruteForce::new().find_satisfying(&problem).is_none()); + assert!(BruteForce::new().find_witness(&problem).is_none()); } #[test] @@ -118,7 +118,7 @@ fn test_undirected_two_commodity_integral_flow_paper_example() { let config = example_config(); assert!(problem.evaluate(&config)); - let all = BruteForce::new().find_all_satisfying(&problem); + let all = BruteForce::new().find_all_witnesses(&problem); assert_eq!(all.len(), 2); assert!(all.contains(&config)); } diff --git a/src/unit_tests/models/misc/additional_key.rs b/src/unit_tests/models/misc/additional_key.rs index da348277e..b66bf9148 100644 --- a/src/unit_tests/models/misc/additional_key.rs +++ b/src/unit_tests/models/misc/additional_key.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; /// Instance 1: 6 attributes, cyclic FDs, 3 known keys. @@ -81,7 +81,7 @@ fn test_additional_key_no_additional_key() { let problem = instance2(); // Only candidate key is {0}, which is already known. let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -103,7 +103,7 @@ fn test_additional_key_brute_force() { let problem = instance1(); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -112,7 +112,7 @@ fn test_additional_key_brute_force() { fn test_additional_key_brute_force_all() { let problem = instance1(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // Exactly 2 additional keys: {0,2} and {0,3,5} assert_eq!(solutions.len(), 2); for sol in &solutions { diff --git a/src/unit_tests/models/misc/bin_packing.rs b/src/unit_tests/models/misc/bin_packing.rs index 079e62da4..cb1c7568f 100644 --- a/src/unit_tests/models/misc/bin_packing.rs +++ b/src/unit_tests/models/misc/bin_packing.rs @@ -1,7 +1,6 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::solvers::BruteForce; +use crate::traits::Problem; #[test] fn test_bin_packing_creation() { @@ -14,12 +13,6 @@ fn test_bin_packing_creation() { assert!(problem.dims().iter().all(|&d| d == 6)); } -#[test] -fn test_bin_packing_direction() { - let problem = BinPacking::new(vec![1, 2, 3], 5); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_bin_packing_evaluate_valid() { // 6 items, capacity 10, sizes [6, 6, 5, 5, 4, 4] @@ -70,7 +63,9 @@ fn test_bin_packing_brute_force_solver() { // Optimal: 3 bins (lower bound ceil(30/10) = 3) let problem = BinPacking::new(vec![6, 6, 5, 5, 4, 4], 10); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); let metric = problem.evaluate(&solution); assert!(metric.is_valid()); assert_eq!(metric.unwrap(), 3); @@ -82,7 +77,9 @@ fn test_bin_packing_brute_force_small() { // Optimal: 2 bins (e.g., {3,4} + {3}) let problem = BinPacking::new(vec![3, 3, 4], 7); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); let metric = problem.evaluate(&solution); assert!(metric.is_valid()); assert_eq!(metric.unwrap(), 2); diff --git a/src/unit_tests/models/misc/boyce_codd_normal_form_violation.rs b/src/unit_tests/models/misc/boyce_codd_normal_form_violation.rs index c41ee0f94..d036f415e 100644 --- a/src/unit_tests/models/misc/boyce_codd_normal_form_violation.rs +++ b/src/unit_tests/models/misc/boyce_codd_normal_form_violation.rs @@ -66,7 +66,7 @@ fn test_bcnf_evaluate_invalid_config_values() { fn test_bcnf_solver_finds_violation() { let problem = canonical_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); // All returned solutions must evaluate to true. for sol in &solutions { @@ -81,7 +81,7 @@ fn test_bcnf_no_violation_when_fds_trivial() { // Only trivial FD: {0} → {0}. No non-trivial closure possible. let problem = BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![0])], vec![0, 1, 2]); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.is_empty()); } @@ -187,7 +187,7 @@ fn test_bcnf_cyclic_keys_no_violation() { vec![0, 1, 2, 3], ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!( solutions.is_empty(), "Cyclic-key instance should have no BCNF violation" diff --git a/src/unit_tests/models/misc/capacity_assignment.rs b/src/unit_tests/models/misc/capacity_assignment.rs index 46fcf78bc..ed9f04932 100644 --- a/src/unit_tests/models/misc/capacity_assignment.rs +++ b/src/unit_tests/models/misc/capacity_assignment.rs @@ -45,7 +45,7 @@ fn test_capacity_assignment_rejects_invalid_configs() { fn test_capacity_assignment_bruteforce_solution_count() { let problem = example_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 5); assert!(solutions.contains(&vec![1, 1, 1])); assert!(solutions.contains(&vec![0, 1, 2])); @@ -70,7 +70,7 @@ fn test_capacity_assignment_paper_example() { assert!(problem.evaluate(&config)); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 5); assert!(solutions.contains(&config)); } diff --git a/src/unit_tests/models/misc/conjunctive_boolean_query.rs b/src/unit_tests/models/misc/conjunctive_boolean_query.rs index 3da1f3cfc..3ca9e701e 100644 --- a/src/unit_tests/models/misc/conjunctive_boolean_query.rs +++ b/src/unit_tests/models/misc/conjunctive_boolean_query.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; /// Helper to build the issue example instance. @@ -85,7 +85,7 @@ fn test_conjunctivebooleanquery_brute_force() { let problem = issue_example(); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -105,7 +105,7 @@ fn test_conjunctivebooleanquery_unsatisfiable() { ]; let problem = ConjunctiveBooleanQuery::new(2, relations, 1, conjuncts); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -121,7 +121,7 @@ fn test_conjunctivebooleanquery_paper_example() { // Same instance as the issue example — count all satisfying assignments let problem = issue_example(); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); // (0,1) satisfies; verify count manually: // For each (y0, y1) in {0..5}x{0..5}: // need R_0(y0, 3) and R_0(y1, 3) and R_1(y0, y1, 5) diff --git a/src/unit_tests/models/misc/conjunctive_query_foldability.rs b/src/unit_tests/models/misc/conjunctive_query_foldability.rs index 82daba5ae..b465cd46d 100644 --- a/src/unit_tests/models/misc/conjunctive_query_foldability.rs +++ b/src/unit_tests/models/misc/conjunctive_query_foldability.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; /// Build the YES instance (foldable): @@ -92,14 +92,14 @@ fn test_conjunctive_query_foldability_yes_instance() { fn test_conjunctive_query_foldability_no_instance() { let problem = no_instance(); // No substitution σ on {U0, U1, U2} maps the triangle into a 2-cycle - let result = BruteForce::new().find_satisfying(&problem); + let result = BruteForce::new().find_witness(&problem); assert_eq!(result, None); } #[test] fn test_conjunctive_query_foldability_solver() { let problem = yes_instance(); - let result = BruteForce::new().find_satisfying(&problem); + let result = BruteForce::new().find_witness(&problem); assert!( result.is_some(), "YES instance must have a satisfying config" @@ -141,7 +141,7 @@ fn test_conjunctive_query_foldability_paper_example() { // Enumerate all satisfying configs. // U(2) (= a) does not appear in Q1, so σ(U2) is a free choice (4 values). // Only σ(U0)=3 and σ(U1)=3 are required; σ(U2) can be anything in 0..4. - let all = BruteForce::new().find_all_satisfying(&problem); + let all = BruteForce::new().find_all_witnesses(&problem); assert_eq!( all.len(), 4, diff --git a/src/unit_tests/models/misc/consistency_of_database_frequency_tables.rs b/src/unit_tests/models/misc/consistency_of_database_frequency_tables.rs index 7826720ef..949f2c75a 100644 --- a/src/unit_tests/models/misc/consistency_of_database_frequency_tables.rs +++ b/src/unit_tests/models/misc/consistency_of_database_frequency_tables.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_yes_instance() -> ConsistencyOfDatabaseFrequencyTables { @@ -113,7 +113,7 @@ fn test_cdft_bruteforce_finds_small_satisfying_assignment() { let problem = small_yes_instance(); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("small instance should be satisfiable"); assert!(problem.evaluate(&solution)); } @@ -122,7 +122,7 @@ fn test_cdft_bruteforce_finds_small_satisfying_assignment() { fn test_cdft_bruteforce_detects_small_unsat_instance() { let problem = small_no_instance(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] diff --git a/src/unit_tests/models/misc/ensemble_computation.rs b/src/unit_tests/models/misc/ensemble_computation.rs index aa19b48bb..eed2489eb 100644 --- a/src/unit_tests/models/misc/ensemble_computation.rs +++ b/src/unit_tests/models/misc/ensemble_computation.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_problem() -> EnsembleComputation { @@ -62,11 +62,11 @@ fn test_ensemble_computation_small_bruteforce_instance() { let problem = EnsembleComputation::new(2, vec![vec![0, 1]], 1); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&problem); + let satisfying = solver.find_all_witnesses(&problem); assert_eq!(satisfying.len(), 2); assert!(satisfying.contains(&vec![0, 1])); assert!(satisfying.contains(&vec![1, 0])); - assert_eq!(solver.find_satisfying(&problem), Some(vec![0, 1])); + assert_eq!(solver.find_witness(&problem), Some(vec![0, 1])); } #[test] diff --git a/src/unit_tests/models/misc/expected_retrieval_cost.rs b/src/unit_tests/models/misc/expected_retrieval_cost.rs index 193e90f9c..9eb95fa8c 100644 --- a/src/unit_tests/models/misc/expected_retrieval_cost.rs +++ b/src/unit_tests/models/misc/expected_retrieval_cost.rs @@ -1,5 +1,5 @@ use super::ExpectedRetrievalCost; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; const EPS: f64 = 1e-9; @@ -66,7 +66,7 @@ fn test_expected_retrieval_cost_rejects_invalid_configs() { fn test_expected_retrieval_cost_solver_finds_satisfying_assignment() { let problem = yes_problem(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem).unwrap(); + let solution = solver.find_witness(&problem).unwrap(); assert!(problem.evaluate(&solution)); } @@ -77,7 +77,7 @@ fn test_expected_retrieval_cost_paper_example() { assert!(problem.evaluate(&config)); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&problem); + let satisfying = solver.find_all_witnesses(&problem); assert_eq!(satisfying.len(), 54); } diff --git a/src/unit_tests/models/misc/factoring.rs b/src/unit_tests/models/misc/factoring.rs index d2f8aa0d6..61deada65 100644 --- a/src/unit_tests/models/misc/factoring.rs +++ b/src/unit_tests/models/misc/factoring.rs @@ -1,7 +1,6 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::Problem; include!("../../jl_helpers.rs"); #[test] @@ -50,12 +49,6 @@ fn test_is_factoring_function() { assert!(!is_factoring(6, 2, 2)); } -#[test] -fn test_direction() { - let problem = Factoring::new(2, 2, 6); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_is_valid_factorization() { let problem = Factoring::new(2, 2, 6); @@ -90,7 +83,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "Factoring best solutions mismatch"); diff --git a/src/unit_tests/models/misc/flow_shop_scheduling.rs b/src/unit_tests/models/misc/flow_shop_scheduling.rs index 58b0490a3..916368a70 100644 --- a/src/unit_tests/models/misc/flow_shop_scheduling.rs +++ b/src/unit_tests/models/misc/flow_shop_scheduling.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -121,7 +121,7 @@ fn test_flow_shop_scheduling_brute_force_solver() { // Small instance: 2 machines, 3 jobs, generous deadline let problem = FlowShopScheduling::new(2, vec![vec![3, 2], vec![2, 4], vec![1, 3]], 20); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let config = solution.unwrap(); assert!(problem.evaluate(&config)); @@ -137,7 +137,7 @@ fn test_flow_shop_scheduling_brute_force_unsatisfiable() { // Deadline 10 < 15 => unsatisfiable let problem = FlowShopScheduling::new(2, vec![vec![5, 5], vec![5, 5]], 10); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -151,7 +151,7 @@ fn test_flow_shop_scheduling_empty() { } #[test] -fn test_flow_shop_scheduling_find_all_satisfying() { +fn test_flow_shop_scheduling_find_all_witnesses() { // Issue #507 example: 3 machines, 5 jobs, D=25 // Search space = 5! = 120 permutations let problem = FlowShopScheduling::new( @@ -166,7 +166,7 @@ fn test_flow_shop_scheduling_find_all_satisfying() { 25, ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); } @@ -178,12 +178,12 @@ fn test_flow_shop_scheduling_find_all_satisfying() { } #[test] -fn test_flow_shop_scheduling_find_all_satisfying_empty() { +fn test_flow_shop_scheduling_find_all_witnesses_empty() { // 2 machines, 2 symmetric jobs [5,5], deadline 10 // Both orderings give makespan 15 > 10 let problem = FlowShopScheduling::new(2, vec![vec![5, 5], vec![5, 5]], 10); let solver = BruteForce::new(); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] diff --git a/src/unit_tests/models/misc/grouping_by_swapping.rs b/src/unit_tests/models/misc/grouping_by_swapping.rs index f3d2dfecc..1436988be 100644 --- a/src/unit_tests/models/misc/grouping_by_swapping.rs +++ b/src/unit_tests/models/misc/grouping_by_swapping.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_yes_instance() -> GroupingBySwapping { @@ -60,16 +60,16 @@ fn test_grouping_by_swapping_bruteforce_yes_and_no() { let solver = BruteForce::new(); let satisfying = solver - .find_satisfying(&yes_problem) + .find_witness(&yes_problem) .expect("expected a satisfying 3-swap sequence"); assert!(yes_problem.evaluate(&satisfying)); assert!(solver - .find_all_satisfying(&yes_problem) + .find_all_witnesses(&yes_problem) .iter() .any(|config| config == &vec![2, 1, 3])); - assert!(solver.find_satisfying(&no_problem).is_none()); - assert!(solver.find_all_satisfying(&no_problem).is_empty()); + assert!(solver.find_witness(&no_problem).is_none()); + assert!(solver.find_all_witnesses(&no_problem).is_empty()); } #[test] @@ -79,10 +79,10 @@ fn test_grouping_by_swapping_paper_example() { let solver = BruteForce::new(); assert!(solver - .find_all_satisfying(&problem) + .find_all_witnesses(&problem) .iter() .any(|config| config == &vec![2, 1, 3, 5, 5])); - assert!(solver.find_satisfying(&issue_two_swap_instance()).is_none()); + assert!(solver.find_witness(&issue_two_swap_instance()).is_none()); } #[test] diff --git a/src/unit_tests/models/misc/knapsack.rs b/src/unit_tests/models/misc/knapsack.rs index b87505fa0..ec75b7077 100644 --- a/src/unit_tests/models/misc/knapsack.rs +++ b/src/unit_tests/models/misc/knapsack.rs @@ -1,7 +1,6 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::solvers::BruteForce; +use crate::traits::Problem; #[test] fn test_knapsack_basic() { @@ -11,7 +10,6 @@ fn test_knapsack_basic() { assert_eq!(problem.values(), &[3, 4, 5, 7]); assert_eq!(problem.capacity(), 7); assert_eq!(problem.dims(), vec![2; 4]); - assert_eq!(problem.direction(), Direction::Maximize); assert_eq!(::NAME, "Knapsack"); assert_eq!(::variant(), vec![]); } @@ -19,44 +17,44 @@ fn test_knapsack_basic() { #[test] fn test_knapsack_evaluate_optimal() { let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); - assert_eq!(problem.evaluate(&[1, 0, 0, 1]), SolutionSize::Valid(10)); + assert_eq!(problem.evaluate(&[1, 0, 0, 1]), Max(Some(10))); } #[test] fn test_knapsack_evaluate_feasible() { let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); - assert_eq!(problem.evaluate(&[1, 1, 0, 0]), SolutionSize::Valid(7)); + assert_eq!(problem.evaluate(&[1, 1, 0, 0]), Max(Some(7))); } #[test] fn test_knapsack_evaluate_overweight() { let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); - assert_eq!(problem.evaluate(&[0, 0, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 1, 1]), Max(None)); } #[test] fn test_knapsack_evaluate_empty() { let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); - assert_eq!(problem.evaluate(&[0, 0, 0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0, 0, 0, 0]), Max(Some(0))); } #[test] fn test_knapsack_evaluate_all_selected() { let problem = Knapsack::new(vec![1, 1, 1], vec![10, 20, 30], 5); - assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Valid(60)); + assert_eq!(problem.evaluate(&[1, 1, 1]), Max(Some(60))); } #[test] fn test_knapsack_evaluate_wrong_config_length() { let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5); - assert_eq!(problem.evaluate(&[1]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[1, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1]), Max(None)); + assert_eq!(problem.evaluate(&[1, 0, 0]), Max(None)); } #[test] fn test_knapsack_evaluate_invalid_variable_value() { let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5); - assert_eq!(problem.evaluate(&[2, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[2, 0]), Max(None)); } #[test] @@ -64,16 +62,18 @@ fn test_knapsack_empty_instance() { let problem = Knapsack::new(vec![], vec![], 10); assert_eq!(problem.num_items(), 0); assert_eq!(problem.dims(), Vec::::new()); - assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[]), Max(Some(0))); } #[test] fn test_knapsack_brute_force() { let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); let metric = problem.evaluate(&solution); - assert_eq!(metric, SolutionSize::Valid(10)); + assert_eq!(metric, Max(Some(10))); } #[test] @@ -90,22 +90,22 @@ fn test_knapsack_serialization() { fn test_knapsack_zero_capacity() { // Capacity 0: only empty set is feasible let problem = Knapsack::new(vec![1, 2], vec![10, 20], 0); - assert_eq!(problem.evaluate(&[0, 0]), SolutionSize::Valid(0)); - assert_eq!(problem.evaluate(&[1, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0]), Max(Some(0))); + assert_eq!(problem.evaluate(&[1, 0]), Max(None)); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(0)); + let solution = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), Max(Some(0))); } #[test] fn test_knapsack_single_item() { // Single item that fits let problem = Knapsack::new(vec![3], vec![5], 3); - assert_eq!(problem.evaluate(&[1]), SolutionSize::Valid(5)); - assert_eq!(problem.evaluate(&[0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[1]), Max(Some(5))); + assert_eq!(problem.evaluate(&[0]), Max(Some(0))); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(5)); + let solution = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), Max(Some(5))); } #[test] @@ -117,8 +117,8 @@ fn test_knapsack_greedy_not_optimal() { // Capacity=10. Greedy: {0} value=7. Optimal: {1,2} value=10. let problem = Knapsack::new(vec![6, 5, 5], vec![7, 5, 5], 10); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(10)); + let solution = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), Max(Some(10))); } #[test] diff --git a/src/unit_tests/models/misc/longest_common_subsequence.rs b/src/unit_tests/models/misc/longest_common_subsequence.rs index edc660b2a..ef4a52a87 100644 --- a/src/unit_tests/models/misc/longest_common_subsequence.rs +++ b/src/unit_tests/models/misc/longest_common_subsequence.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_yes_instance() -> LongestCommonSubsequence { @@ -82,7 +82,7 @@ fn test_lcs_bruteforce_yes() { let problem = issue_yes_instance(); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("expected a common subsequence witness"); assert!(problem.evaluate(&solution)); } @@ -91,14 +91,14 @@ fn test_lcs_bruteforce_yes() { fn test_lcs_bruteforce_no() { let problem = issue_no_instance(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] -fn test_lcs_find_all_satisfying_contains_issue_witness() { +fn test_lcs_find_all_witnesses_contains_issue_witness() { let problem = issue_yes_instance(); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&problem); + let satisfying = solver.find_all_witnesses(&problem); assert!(satisfying.iter().any(|config| config == &vec![0, 1, 0])); } @@ -117,7 +117,7 @@ fn test_lcs_paper_example() { assert!(problem.evaluate(&[0, 1, 0])); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&problem); + let satisfying = solver.find_all_witnesses(&problem); assert!(!satisfying.is_empty()); } diff --git a/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs b/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs index 6d85c5ace..fdcf29b64 100644 --- a/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs +++ b/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs @@ -1,7 +1,6 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::solvers::BruteForce; +use crate::traits::Problem; #[test] fn test_minimum_tardiness_sequencing_basic() { @@ -15,7 +14,6 @@ fn test_minimum_tardiness_sequencing_basic() { assert_eq!(problem.precedences(), &[(0, 3), (1, 3), (1, 4), (2, 4)]); assert_eq!(problem.num_precedences(), 4); assert_eq!(problem.dims(), vec![5, 4, 3, 2, 1]); - assert_eq!(problem.direction(), Direction::Minimize); assert_eq!( ::NAME, "MinimumTardinessSequencing" @@ -37,28 +35,28 @@ fn test_minimum_tardiness_sequencing_evaluate_optimal() { // sigma: task 0 at pos 0, task 1 at pos 1, task 3 at pos 2, task 2 at pos 3, task 4 at pos 4. // t0 finishes at 1 <= 5, t1 at 2 <= 5, t3 at 3 <= 3, t2 at 4 <= 5, t4 at 5 > 3 (tardy) let config = vec![0, 0, 1, 0, 0]; - assert_eq!(problem.evaluate(&config), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&config), Min(Some(1))); } #[test] fn test_minimum_tardiness_sequencing_evaluate_invalid_lehmer() { let problem = MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![]); // dims = [3, 2, 1]; config [0, 2, 0] has 2 >= 2 (second dim), invalid Lehmer code - assert_eq!(problem.evaluate(&[0, 2, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 2, 0]), Min(None)); } #[test] fn test_minimum_tardiness_sequencing_evaluate_out_of_range() { let problem = MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![]); // dims = [3, 2, 1]; config [0, 1, 5] has 5 >= 1 (third dim), out of range - assert_eq!(problem.evaluate(&[0, 1, 5]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 1, 5]), Min(None)); } #[test] fn test_minimum_tardiness_sequencing_evaluate_wrong_length() { let problem = MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![]); - assert_eq!(problem.evaluate(&[0, 1]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[0, 1, 2, 3]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 1]), Min(None)); + assert_eq!(problem.evaluate(&[0, 1, 2, 3]), Min(None)); } #[test] @@ -69,11 +67,11 @@ fn test_minimum_tardiness_sequencing_evaluate_precedence_violation() { vec![(0, 1)], // task 0 must precede task 1 ); // Lehmer [0,0,0] -> schedule [0,1,2] -> sigma [0,1,2]: sigma(0)=0 < sigma(1)=1, valid - assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0, 0, 0]), Min(Some(0))); // Lehmer [1,0,0] -> schedule [1,0,2] -> sigma [1,0,2]: sigma(0)=1 >= sigma(1)=0, violates - assert_eq!(problem.evaluate(&[1, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 0]), Min(None)); // Lehmer [2,1,0] -> schedule [2,1,0] -> sigma [2,1,0]: sigma(0)=2 >= sigma(1)=1, violates - assert_eq!(problem.evaluate(&[2, 1, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[2, 1, 0]), Min(None)); } #[test] @@ -81,9 +79,9 @@ fn test_minimum_tardiness_sequencing_evaluate_all_on_time() { let problem = MinimumTardinessSequencing::new(3, vec![3, 3, 3], vec![]); // All deadlines are 3, so any permutation of 3 tasks is on time // Lehmer [0,0,0] -> schedule [0,1,2] - assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0, 0, 0]), Min(Some(0))); // Lehmer [2,1,0] -> schedule [2,1,0] - assert_eq!(problem.evaluate(&[2, 1, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[2, 1, 0]), Min(Some(0))); } #[test] @@ -94,7 +92,7 @@ fn test_minimum_tardiness_sequencing_evaluate_all_tardy() { let problem = MinimumTardinessSequencing::new(2, vec![0, 0], vec![]); // Lehmer [0,0] -> schedule [0,1] -> sigma [0,1] // pos 0 finishes at 1 > 0 (tardy), pos 1 finishes at 2 > 0 (tardy) - assert_eq!(problem.evaluate(&[0, 0]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[0, 0]), Min(Some(2))); } #[test] @@ -105,10 +103,12 @@ fn test_minimum_tardiness_sequencing_brute_force() { vec![(0, 3), (1, 3), (1, 4), (2, 4)], ); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); let metric = problem.evaluate(&solution); // Optimal is 1 tardy task - assert_eq!(metric, SolutionSize::Valid(1)); + assert_eq!(metric, Min(Some(1))); } #[test] @@ -117,10 +117,12 @@ fn test_minimum_tardiness_sequencing_brute_force_no_precedences() { // 3 tasks: deadlines 1, 3, 2. Best is to schedule task with deadline 1 first. let problem = MinimumTardinessSequencing::new(3, vec![1, 3, 2], vec![]); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); let metric = problem.evaluate(&solution); // All can be on time: t0 at pos 0 (finish 1 <= 1), t2 at pos 1 (finish 2 <= 2), t1 at pos 2 (finish 3 <= 3) - assert_eq!(metric, SolutionSize::Valid(0)); + assert_eq!(metric, Min(Some(0))); } #[test] @@ -138,7 +140,7 @@ fn test_minimum_tardiness_sequencing_empty() { let problem = MinimumTardinessSequencing::new(0, vec![], vec![]); assert_eq!(problem.num_tasks(), 0); assert_eq!(problem.dims(), Vec::::new()); - assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[]), Min(Some(0))); } #[test] @@ -146,11 +148,11 @@ fn test_minimum_tardiness_sequencing_single_task() { let problem = MinimumTardinessSequencing::new(1, vec![1], vec![]); assert_eq!(problem.dims(), vec![1]); // Task at position 0, finishes at 1 <= 1, not tardy - assert_eq!(problem.evaluate(&[0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0]), Min(Some(0))); let problem_tardy = MinimumTardinessSequencing::new(1, vec![0], vec![]); // Task at position 0, finishes at 1 > 0, tardy - assert_eq!(problem_tardy.evaluate(&[0]), SolutionSize::Valid(1)); + assert_eq!(problem_tardy.evaluate(&[0]), Min(Some(1))); } #[test] @@ -170,5 +172,5 @@ fn test_minimum_tardiness_sequencing_cyclic_precedences() { // Cyclic precedences: 0 -> 1 -> 2 -> 0. No valid schedule exists. let problem = MinimumTardinessSequencing::new(3, vec![3, 3, 3], vec![(0, 1), (1, 2), (2, 0)]); let solver = BruteForce::new(); - assert!(solver.find_best(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } diff --git a/src/unit_tests/models/misc/multiprocessor_scheduling.rs b/src/unit_tests/models/misc/multiprocessor_scheduling.rs index ee37b078e..bbc5ff3d8 100644 --- a/src/unit_tests/models/misc/multiprocessor_scheduling.rs +++ b/src/unit_tests/models/misc/multiprocessor_scheduling.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -91,7 +91,7 @@ fn test_multiprocessor_scheduling_three_processors() { fn test_multiprocessor_scheduling_brute_force() { let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let config = solution.unwrap(); assert!(problem.evaluate(&config)); @@ -102,17 +102,17 @@ fn test_multiprocessor_scheduling_brute_force_infeasible() { // Total length = 20, with 2 processors and deadline 9, impossible let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 9); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } #[test] -fn test_multiprocessor_scheduling_find_all_satisfying() { +fn test_multiprocessor_scheduling_find_all_witnesses() { // Issue #212 example: 5 tasks [4,5,3,2,6], m=2, D=10 // Search space = 2^5 = 32 let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); } @@ -123,12 +123,12 @@ fn test_multiprocessor_scheduling_find_all_satisfying() { } #[test] -fn test_multiprocessor_scheduling_find_all_satisfying_empty() { +fn test_multiprocessor_scheduling_find_all_witnesses_empty() { // Same instance but deadline 9: total=20, need each processor ≤ 9, // but 20 > 2*9 = 18, so impossible let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 9); let solver = BruteForce::new(); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] diff --git a/src/unit_tests/models/misc/paintshop.rs b/src/unit_tests/models/misc/paintshop.rs index 87c0f1607..6469f3118 100644 --- a/src/unit_tests/models/misc/paintshop.rs +++ b/src/unit_tests/models/misc/paintshop.rs @@ -1,7 +1,6 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::solvers::BruteForce; +use crate::traits::Problem; include!("../../jl_helpers.rs"); #[test] @@ -55,18 +54,12 @@ fn test_count_paint_switches_function() { assert_eq!(count_paint_switches(&[0, 1, 0, 1]), 3); } -#[test] -fn test_direction() { - let problem = PaintShop::new(vec!["a", "a"]); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_single_car() { let problem = PaintShop::new(vec!["a", "a"]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Both configs give 1 switch: a(0)->a(1) or a(1)->a(0) assert_eq!(solutions.len(), 2); for sol in &solutions { @@ -80,7 +73,7 @@ fn test_adjacent_same_car() { let problem = PaintShop::new(vec!["a", "a", "b", "b"]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Best case: [0,0] -> [0,1,0,1] = 3 switches, or [0,1] -> [0,1,1,0] = 2 switches // Actually: [0,0] -> a=0,a=1,b=0,b=1 = [0,1,0,1] = 3 switches // [0,1] -> a=0,a=1,b=1,b=0 = [0,1,1,0] = 2 switches @@ -124,7 +117,7 @@ fn test_jl_parity_evaluation() { config ); } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "PaintShop best solutions mismatch"); @@ -148,6 +141,6 @@ fn test_paintshop_paper_example() { // Config [0, 0, 1]: A first=0, B first=0, C first=1 // Coloring: A(0), B(0), A(1), C(1), B(1), C(0) -> [0,0,1,1,1,0] -> 2 switches let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 2); } diff --git a/src/unit_tests/models/misc/partially_ordered_knapsack.rs b/src/unit_tests/models/misc/partially_ordered_knapsack.rs index 013dff32d..9c8f907f2 100644 --- a/src/unit_tests/models/misc/partially_ordered_knapsack.rs +++ b/src/unit_tests/models/misc/partially_ordered_knapsack.rs @@ -1,7 +1,6 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::solvers::BruteForce; +use crate::traits::Problem; /// Helper: create the example instance from the issue. /// Items: a=0, b=1, c=2, d=3, e=4, f=5 @@ -30,7 +29,6 @@ fn test_partially_ordered_knapsack_basic() { ); assert_eq!(problem.capacity(), 11); assert_eq!(problem.dims(), vec![2; 6]); - assert_eq!(problem.direction(), Direction::Maximize); assert_eq!( ::NAME, "PartiallyOrderedKnapsack" @@ -44,10 +42,7 @@ fn test_partially_ordered_knapsack_evaluate_valid() { // U' = {a, b, d, e, f} = indices {0, 1, 3, 4, 5} // Total size: 2+3+1+2+3 = 11 <= 11 // Total value: 3+2+4+3+8 = 20 - assert_eq!( - problem.evaluate(&[1, 1, 0, 1, 1, 1]), - SolutionSize::Valid(20) - ); + assert_eq!(problem.evaluate(&[1, 1, 0, 1, 1, 1]), Max(Some(20))); } #[test] @@ -55,7 +50,7 @@ fn test_partially_ordered_knapsack_evaluate_precedence_violation() { let problem = example_instance(); // U' = {d, f} = indices {3, 5} — f requires e and b (transitively), d requires a // Not downward-closed: d selected but a (predecessor of d) not selected - assert_eq!(problem.evaluate(&[0, 0, 0, 1, 0, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 0, 1, 0, 1]), Max(None)); } #[test] @@ -64,7 +59,7 @@ fn test_partially_ordered_knapsack_evaluate_transitive_precedence_violation() { // U' = {d, e, f} = indices {3, 4, 5} // f requires d (ok) and e (ok), but d requires a (0) which is not selected // Also e requires b (1) which is not selected - assert_eq!(problem.evaluate(&[0, 0, 0, 1, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 0, 1, 1, 1]), Max(None)); } #[test] @@ -72,26 +67,20 @@ fn test_partially_ordered_knapsack_evaluate_overweight() { let problem = example_instance(); // U' = {a, b, c, d, e, f} = all items // Total size: 2+3+4+1+2+3 = 15 > 11 - assert_eq!(problem.evaluate(&[1, 1, 1, 1, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1, 1, 1, 1]), Max(None)); } #[test] fn test_partially_ordered_knapsack_evaluate_empty() { let problem = example_instance(); - assert_eq!( - problem.evaluate(&[0, 0, 0, 0, 0, 0]), - SolutionSize::Valid(0) - ); + assert_eq!(problem.evaluate(&[0, 0, 0, 0, 0, 0]), Max(Some(0))); } #[test] fn test_partially_ordered_knapsack_evaluate_single_root() { let problem = example_instance(); // Just item a (no predecessors) - assert_eq!( - problem.evaluate(&[1, 0, 0, 0, 0, 0]), - SolutionSize::Valid(3) - ); + assert_eq!(problem.evaluate(&[1, 0, 0, 0, 0, 0]), Max(Some(3))); } #[test] @@ -100,36 +89,32 @@ fn test_partially_ordered_knapsack_evaluate_valid_chain() { // U' = {a, d} = indices {0, 3} // a has no predecessors, d's predecessor a is selected: downward-closed // Total size: 2+1 = 3 <= 11, Total value: 3+4 = 7 - assert_eq!( - problem.evaluate(&[1, 0, 0, 1, 0, 0]), - SolutionSize::Valid(7) - ); + assert_eq!(problem.evaluate(&[1, 0, 0, 1, 0, 0]), Max(Some(7))); } #[test] fn test_partially_ordered_knapsack_evaluate_wrong_config_length() { let problem = example_instance(); - assert_eq!(problem.evaluate(&[1, 0]), SolutionSize::Invalid); - assert_eq!( - problem.evaluate(&[1, 0, 0, 0, 0, 0, 0]), - SolutionSize::Invalid - ); + assert_eq!(problem.evaluate(&[1, 0]), Max(None)); + assert_eq!(problem.evaluate(&[1, 0, 0, 0, 0, 0, 0]), Max(None)); } #[test] fn test_partially_ordered_knapsack_evaluate_invalid_variable_value() { let problem = example_instance(); - assert_eq!(problem.evaluate(&[2, 0, 0, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[2, 0, 0, 0, 0, 0]), Max(None)); } #[test] fn test_partially_ordered_knapsack_brute_force() { let problem = example_instance(); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); let metric = problem.evaluate(&solution); // The optimal should be {a, b, d, e, f} with value 20 - assert_eq!(metric, SolutionSize::Valid(20)); + assert_eq!(metric, Max(Some(20))); } #[test] @@ -138,7 +123,7 @@ fn test_partially_ordered_knapsack_empty_instance() { assert_eq!(problem.num_items(), 0); assert_eq!(problem.num_precedences(), 0); assert_eq!(problem.dims(), Vec::::new()); - assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[]), Max(Some(0))); } #[test] @@ -146,20 +131,22 @@ fn test_partially_ordered_knapsack_no_precedences() { // Without precedences, behaves like standard knapsack let problem = PartiallyOrderedKnapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], vec![], 7); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); let metric = problem.evaluate(&solution); // Same as standard knapsack: items 0 and 3 give weight 7, value 10 - assert_eq!(metric, SolutionSize::Valid(10)); + assert_eq!(metric, Max(Some(10))); } #[test] fn test_partially_ordered_knapsack_zero_capacity() { let problem = PartiallyOrderedKnapsack::new(vec![1, 2], vec![10, 20], vec![(0, 1)], 0); - assert_eq!(problem.evaluate(&[0, 0]), SolutionSize::Valid(0)); - assert_eq!(problem.evaluate(&[1, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0]), Max(Some(0))); + assert_eq!(problem.evaluate(&[1, 0]), Max(None)); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(0)); + let solution = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), Max(Some(0))); } #[test] diff --git a/src/unit_tests/models/misc/partition.rs b/src/unit_tests/models/misc/partition.rs index 308d27d49..3f031550f 100644 --- a/src/unit_tests/models/misc/partition.rs +++ b/src/unit_tests/models/misc/partition.rs @@ -1,5 +1,5 @@ use crate::models::misc::Partition; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -45,14 +45,14 @@ fn test_partition_odd_total() { // Total = 7 (odd), no equal partition possible let problem = Partition::new(vec![3, 1, 2, 1]); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] fn test_partition_solver() { let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } @@ -61,7 +61,7 @@ fn test_partition_solver() { fn test_partition_solver_all() { let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // 10 satisfying configs for {3,1,1,2,2,1} with target half-sum 5 assert_eq!(solutions.len(), 10); for sol in &solutions { @@ -74,14 +74,14 @@ fn test_partition_single_element() { // Single element can never be partitioned equally let problem = Partition::new(vec![5]); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] fn test_partition_two_equal_elements() { let problem = Partition::new(vec![4, 4]); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } diff --git a/src/unit_tests/models/misc/precedence_constrained_scheduling.rs b/src/unit_tests/models/misc/precedence_constrained_scheduling.rs index 8a3ace315..8c30bc4b3 100644 --- a/src/unit_tests/models/misc/precedence_constrained_scheduling.rs +++ b/src/unit_tests/models/misc/precedence_constrained_scheduling.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -78,7 +78,7 @@ fn test_precedence_constrained_scheduling_brute_force() { let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -87,7 +87,7 @@ fn test_precedence_constrained_scheduling_brute_force() { fn test_precedence_constrained_scheduling_brute_force_all() { let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -99,7 +99,7 @@ fn test_precedence_constrained_scheduling_unsatisfiable() { // 3 tasks in a chain t0 < t1 < t2, but only deadline 2 (need 3 slots) let problem = PrecedenceConstrainedScheduling::new(3, 1, 2, vec![(0, 1), (1, 2)]); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -129,7 +129,7 @@ fn test_precedence_constrained_scheduling_no_precedences() { assert!(problem.evaluate(&[0, 0, 1, 1])); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } diff --git a/src/unit_tests/models/misc/rectilinear_picture_compression.rs b/src/unit_tests/models/misc/rectilinear_picture_compression.rs index 095bfdee7..7b649496d 100644 --- a/src/unit_tests/models/misc/rectilinear_picture_compression.rs +++ b/src/unit_tests/models/misc/rectilinear_picture_compression.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn two_block_matrix() -> Vec> { @@ -96,7 +96,7 @@ fn test_rectilinear_picture_compression_evaluate_invalid_variable_value() { fn test_rectilinear_picture_compression_issue_matrix_satisfiable() { let problem = RectilinearPictureCompression::new(issue_matrix(), 3); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let sol = solution.unwrap(); assert!(problem.evaluate(&sol)); @@ -106,7 +106,7 @@ fn test_rectilinear_picture_compression_issue_matrix_satisfiable() { fn test_rectilinear_picture_compression_issue_matrix_unsatisfiable() { let problem = RectilinearPictureCompression::new(issue_matrix(), 2); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -115,7 +115,7 @@ fn test_rectilinear_picture_compression_brute_force() { let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -124,7 +124,7 @@ fn test_rectilinear_picture_compression_brute_force() { fn test_rectilinear_picture_compression_brute_force_all() { let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // Two disjoint 2x2 blocks with K=2: exactly one satisfying config [1,1]. assert_eq!(solutions.len(), 1); for sol in &solutions { @@ -200,7 +200,7 @@ fn test_rectilinear_picture_compression_overlapping_rectangles() { assert!(rects.contains(&(0, 0, 1, 0))); assert!(rects.contains(&(0, 0, 0, 1))); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem).unwrap(); + let solution = solver.find_witness(&problem).unwrap(); assert!(problem.evaluate(&solution)); } diff --git a/src/unit_tests/models/misc/resource_constrained_scheduling.rs b/src/unit_tests/models/misc/resource_constrained_scheduling.rs index bd5e194e8..16f352a6b 100644 --- a/src/unit_tests/models/misc/resource_constrained_scheduling.rs +++ b/src/unit_tests/models/misc/resource_constrained_scheduling.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -111,7 +111,7 @@ fn test_resource_constrained_scheduling_brute_force_infeasible() { 2, ); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); // 1 processor * 2 time slots = 2 tasks max, but we have 4 assert!(solution.is_none()); } @@ -170,7 +170,7 @@ fn test_resource_constrained_scheduling_single_task_exceeds_bound() { assert!(!problem.evaluate(&[0])); assert!(!problem.evaluate(&[1])); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -189,7 +189,7 @@ fn test_resource_constrained_scheduling_canonical_brute_force() { 2, ); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert!(!all.is_empty()); // Verify the hardcoded canonical solution is among the brute-force results assert!(all.contains(&vec![0, 0, 0, 1, 1, 1])); diff --git a/src/unit_tests/models/misc/scheduling_with_individual_deadlines.rs b/src/unit_tests/models/misc/scheduling_with_individual_deadlines.rs index 6c46ac905..19ae0cab4 100644 --- a/src/unit_tests/models/misc/scheduling_with_individual_deadlines.rs +++ b/src/unit_tests/models/misc/scheduling_with_individual_deadlines.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_example_problem() -> SchedulingWithIndividualDeadlines { @@ -77,7 +77,7 @@ fn test_scheduling_with_individual_deadlines_evaluate_handles_huge_sparse_deadli let result = std::panic::catch_unwind(|| problem.evaluate(&[0])); - assert!(matches!(result, Ok(true))); + assert!(matches!(result, Ok(crate::types::Or(true)))); } #[test] @@ -85,8 +85,8 @@ fn test_scheduling_with_individual_deadlines_brute_force_satisfiable() { let problem = SchedulingWithIndividualDeadlines::new(3, 2, vec![1, 1, 2], vec![(0, 2)]); let solver = BruteForce::new(); - assert_eq!(solver.find_all_satisfying(&problem), vec![vec![0, 0, 1]]); - assert_eq!(solver.find_satisfying(&problem), Some(vec![0, 0, 1])); + assert_eq!(solver.find_all_witnesses(&problem), vec![vec![0, 0, 1]]); + assert_eq!(solver.find_witness(&problem), Some(vec![0, 0, 1])); } #[test] @@ -94,7 +94,7 @@ fn test_scheduling_with_individual_deadlines_brute_force_unsatisfiable() { let problem = SchedulingWithIndividualDeadlines::new(3, 1, vec![1, 1, 1], vec![]); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -114,14 +114,11 @@ fn test_scheduling_with_individual_deadlines_paper_example() { let problem = issue_example_problem(); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&problem); + let satisfying = solver.find_all_witnesses(&problem); assert!(problem.evaluate(&[0, 0, 0, 1, 2, 1, 1])); assert!(satisfying.contains(&vec![0, 0, 0, 1, 2, 1, 1])); - assert_eq!( - solver.find_satisfying(&problem), - satisfying.into_iter().next() - ); + assert_eq!(solver.find_witness(&problem), satisfying.into_iter().next()); } #[test] diff --git a/src/unit_tests/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs b/src/unit_tests/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs index fa6a4823f..166204ddf 100644 --- a/src/unit_tests/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs +++ b/src/unit_tests/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_example(bound: i64) -> SequencingToMinimizeMaximumCumulativeCost { @@ -76,7 +76,7 @@ fn test_sequencing_to_minimize_maximum_cumulative_cost_brute_force_solver() { let problem = issue_example(4); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a satisfying schedule"); assert!(problem.evaluate(&solution)); } @@ -89,7 +89,7 @@ fn test_sequencing_to_minimize_maximum_cumulative_cost_unsatisfiable_cycle() { 10, ); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -98,7 +98,7 @@ fn test_sequencing_to_minimize_maximum_cumulative_cost_paper_example() { let sample_config = vec![1, 0, 1, 0, 0, 0]; assert!(problem.evaluate(&sample_config)); - let satisfying = BruteForce::new().find_all_satisfying(&problem); + let satisfying = BruteForce::new().find_all_witnesses(&problem); assert_eq!(satisfying.len(), 5); assert!(satisfying.iter().any(|config| config == &sample_config)); } diff --git a/src/unit_tests/models/misc/sequencing_to_minimize_weighted_completion_time.rs b/src/unit_tests/models/misc/sequencing_to_minimize_weighted_completion_time.rs index 08aeab2c3..1c0339215 100644 --- a/src/unit_tests/models/misc/sequencing_to_minimize_weighted_completion_time.rs +++ b/src/unit_tests/models/misc/sequencing_to_minimize_weighted_completion_time.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::Problem; +use crate::types::Min; #[test] fn test_sequencing_to_minimize_weighted_completion_time_basic() { @@ -18,7 +18,6 @@ fn test_sequencing_to_minimize_weighted_completion_time_basic() { assert_eq!(problem.num_precedences(), 2); assert_eq!(problem.total_processing_time(), 9); assert_eq!(problem.dims(), vec![5, 4, 3, 2, 1]); - assert_eq!(problem.direction(), Direction::Minimize); assert_eq!( ::NAME, "SequencingToMinimizeWeightedCompletionTime" @@ -40,7 +39,7 @@ fn test_sequencing_to_minimize_weighted_completion_time_evaluate_issue_example() // Lehmer [1,2,0,1,0] decodes to schedule [1,3,0,4,2]. // Completion times are [4,1,9,2,6], so the objective is // 3*4 + 5*1 + 1*9 + 4*2 + 2*6 = 46. - assert_eq!(problem.evaluate(&[1, 2, 0, 1, 0]), SolutionSize::Valid(46)); + assert_eq!(problem.evaluate(&[1, 2, 0, 1, 0]), Min(Some(46))); } #[test] @@ -48,8 +47,8 @@ fn test_sequencing_to_minimize_weighted_completion_time_evaluate_invalid_lehmer( let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![2, 1, 3], vec![3, 5, 1], vec![]); - assert_eq!(problem.evaluate(&[0, 2, 0]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[0, 1, 5]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 2, 0]), Min(None)); + assert_eq!(problem.evaluate(&[0, 1, 5]), Min(None)); } #[test] @@ -57,8 +56,8 @@ fn test_sequencing_to_minimize_weighted_completion_time_evaluate_wrong_length() let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![2, 1, 3], vec![3, 5, 1], vec![]); - assert_eq!(problem.evaluate(&[0, 1]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[0, 1, 2, 3]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 1]), Min(None)); + assert_eq!(problem.evaluate(&[0, 1, 2, 3]), Min(None)); } #[test] @@ -66,8 +65,8 @@ fn test_sequencing_to_minimize_weighted_completion_time_evaluate_precedence_viol let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![2, 1, 3], vec![3, 5, 1], vec![(0, 1)]); - assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Valid(27)); - assert_eq!(problem.evaluate(&[1, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 0]), Min(Some(27))); + assert_eq!(problem.evaluate(&[1, 0, 0]), Min(None)); } #[test] @@ -78,10 +77,12 @@ fn test_sequencing_to_minimize_weighted_completion_time_brute_force() { vec![(0, 2), (1, 4)], ); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); assert_eq!(solution, vec![1, 2, 0, 1, 0]); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(46)); + assert_eq!(problem.evaluate(&solution), Min(Some(46))); } #[test] @@ -116,7 +117,7 @@ fn test_sequencing_to_minimize_weighted_completion_time_empty() { assert_eq!(problem.num_tasks(), 0); assert_eq!(problem.dims(), Vec::::new()); - assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[]), Min(Some(0))); } #[test] @@ -124,7 +125,7 @@ fn test_sequencing_to_minimize_weighted_completion_time_single_task() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![3], vec![2], vec![]); assert_eq!(problem.dims(), vec![1]); - assert_eq!(problem.evaluate(&[0]), SolutionSize::Valid(6)); + assert_eq!(problem.evaluate(&[0]), Min(Some(6))); } #[test] @@ -154,7 +155,7 @@ fn test_sequencing_to_minimize_weighted_completion_time_cyclic_precedences() { ); let solver = BruteForce::new(); - assert!(solver.find_best(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -166,10 +167,10 @@ fn test_sequencing_to_minimize_weighted_completion_time_paper_example() { ); let expected = vec![1, 2, 0, 1, 0]; - assert_eq!(problem.evaluate(&expected), SolutionSize::Valid(46)); + assert_eq!(problem.evaluate(&expected), Min(Some(46))); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions, vec![expected]); } diff --git a/src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs b/src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs index ca12d961b..b0d45b8f4 100644 --- a/src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs +++ b/src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_example_yes() -> SequencingToMinimizeWeightedTardiness { @@ -77,7 +77,7 @@ fn test_sequencing_to_minimize_weighted_tardiness_solver_yes() { let problem = issue_example_yes(); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a schedule"); assert!(problem.evaluate(&solution)); assert!(problem.total_weighted_tardiness(&solution).unwrap() <= problem.bound()); @@ -87,8 +87,8 @@ fn test_sequencing_to_minimize_weighted_tardiness_solver_yes() { fn test_sequencing_to_minimize_weighted_tardiness_solver_no() { let problem = issue_example_no(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_witness(&problem).is_none()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] @@ -102,9 +102,9 @@ fn test_sequencing_to_minimize_weighted_tardiness_paper_example() { assert!(yes.evaluate(&config)); assert!(!no.evaluate(&config)); - let satisfying = solver.find_all_satisfying(&yes); + let satisfying = solver.find_all_witnesses(&yes); assert_eq!(satisfying, vec![config]); - assert!(solver.find_all_satisfying(&no).is_empty()); + assert!(solver.find_all_witnesses(&no).is_empty()); } #[test] diff --git a/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs b/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs index 96724ba31..48c34ecfc 100644 --- a/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs +++ b/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -35,7 +35,7 @@ fn test_sequencing_rtd_evaluate_feasible() { vec![5, 6, 10, 3, 12], ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // Exactly one feasible schedule exists: Lehmer code [3, 0, 0, 0, 0] assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![3, 0, 0, 0, 0]); @@ -85,7 +85,7 @@ fn test_sequencing_rtd_brute_force() { SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -94,7 +94,7 @@ fn test_sequencing_rtd_brute_force() { fn test_sequencing_rtd_brute_force_all() { let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![0, 0], vec![3, 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -106,7 +106,7 @@ fn test_sequencing_rtd_unsatisfiable() { // Two tasks each need 2 time units but only 3 total time available let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 0], vec![3, 3]); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } diff --git a/src/unit_tests/models/misc/sequencing_within_intervals.rs b/src/unit_tests/models/misc/sequencing_within_intervals.rs index cd4e3a534..a9a60ac2f 100644 --- a/src/unit_tests/models/misc/sequencing_within_intervals.rs +++ b/src/unit_tests/models/misc/sequencing_within_intervals.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -71,7 +71,7 @@ fn test_sequencing_within_intervals_solver() { // Simple instance: 3 tasks that can be scheduled sequentially let problem = SequencingWithinIntervals::new(vec![0, 2, 4], vec![3, 5, 7], vec![2, 2, 2]); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let config = solution.unwrap(); assert!(problem.evaluate(&config)); @@ -86,7 +86,7 @@ fn test_sequencing_within_intervals_solver_canonical() { vec![2, 2, 2, 3, 2], ); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let config = solution.unwrap(); assert!(problem.evaluate(&config)); @@ -101,7 +101,7 @@ fn test_sequencing_within_intervals_no_solution() { // Task 1: start=0, runs [0,2) -> overlap assert!(!problem.evaluate(&[0, 0])); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -149,7 +149,7 @@ fn test_sequencing_within_intervals_single_task() { } #[test] -fn test_sequencing_within_intervals_find_all_satisfying() { +fn test_sequencing_within_intervals_find_all_witnesses() { // Issue #219 canonical instance: 5 tasks with overlapping windows // dims = [4, 6, 5, 4, 11], search space = 5280 let problem = SequencingWithinIntervals::new( @@ -158,7 +158,7 @@ fn test_sequencing_within_intervals_find_all_satisfying() { vec![2, 2, 2, 3, 2], ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); } @@ -168,11 +168,11 @@ fn test_sequencing_within_intervals_find_all_satisfying() { } #[test] -fn test_sequencing_within_intervals_find_all_satisfying_empty() { +fn test_sequencing_within_intervals_find_all_witnesses_empty() { // Two tasks that must both use time [0,2), impossible without overlap let problem = SequencingWithinIntervals::new(vec![0, 0], vec![2, 2], vec![2, 2]); let solver = BruteForce::new(); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] diff --git a/src/unit_tests/models/misc/shortest_common_supersequence.rs b/src/unit_tests/models/misc/shortest_common_supersequence.rs index 5634dcca5..4bceb8e1a 100644 --- a/src/unit_tests/models/misc/shortest_common_supersequence.rs +++ b/src/unit_tests/models/misc/shortest_common_supersequence.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -71,7 +71,7 @@ fn test_shortestcommonsupersequence_brute_force() { let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]], 3); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -91,7 +91,7 @@ fn test_shortestcommonsupersequence_unsatisfiable() { // "01" contains [0,1] but not [1,0]; "10" contains [1,0] but not [0,1] let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]], 2); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -114,16 +114,16 @@ fn test_shortestcommonsupersequence_paper_example() { // Verify a solution exists with brute force let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_some()); + assert!(solver.find_witness(&problem).is_some()); // Bound 3 is too short: LCS("abc","bac")="ac" (len 2), so SCS ≥ 3+3-2 = 4 let tight = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2], vec![1, 0, 2]], 3); let solver2 = BruteForce::new(); - assert!(solver2.find_satisfying(&tight).is_none()); + assert!(solver2.find_witness(&tight).is_none()); } #[test] -fn test_shortestcommonsupersequence_find_all_satisfying() { +fn test_shortestcommonsupersequence_find_all_witnesses() { // Issue #412 instance 1: Σ={a,b,c}, R={"abcb","bcab","acba"}, K=7 // Search space = 3^7 = 2187 let problem = ShortestCommonSupersequence::new( @@ -132,7 +132,7 @@ fn test_shortestcommonsupersequence_find_all_satisfying() { 7, ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); } @@ -142,7 +142,7 @@ fn test_shortestcommonsupersequence_find_all_satisfying() { } #[test] -fn test_shortestcommonsupersequence_find_all_satisfying_empty() { +fn test_shortestcommonsupersequence_find_all_witnesses_empty() { // Issue #412 instance 3: all 6 permutations of {a,b,c}, bound 5 // Minimum SCS length is 7, so bound 5 is infeasible let problem = ShortestCommonSupersequence::new( @@ -158,7 +158,7 @@ fn test_shortestcommonsupersequence_find_all_satisfying_empty() { 5, ); let solver = BruteForce::new(); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] diff --git a/src/unit_tests/models/misc/stacker_crane.rs b/src/unit_tests/models/misc/stacker_crane.rs index 258d45fec..7e70f78b6 100644 --- a/src/unit_tests/models/misc/stacker_crane.rs +++ b/src/unit_tests/models/misc/stacker_crane.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_problem(bound: i32) -> StackerCrane { @@ -58,7 +58,7 @@ fn test_stacker_crane_issue_instance_is_unsatisfiable_at_bound_19() { let problem = issue_problem(19); let solver = BruteForce::new(); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] @@ -70,7 +70,7 @@ fn test_stacker_crane_paper_example() { assert!(problem.evaluate(&witness)); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&problem); + let satisfying = solver.find_all_witnesses(&problem); assert!(!satisfying.is_empty()); assert!(satisfying.contains(&witness)); for config in &satisfying { @@ -84,7 +84,7 @@ fn test_stacker_crane_small_solver_instance() { let solver = BruteForce::new(); let satisfying = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("small instance should be satisfiable"); let mut sorted = satisfying.clone(); sorted.sort_unstable(); diff --git a/src/unit_tests/models/misc/staff_scheduling.rs b/src/unit_tests/models/misc/staff_scheduling.rs index 0ed041a3a..7e36b205f 100644 --- a/src/unit_tests/models/misc/staff_scheduling.rs +++ b/src/unit_tests/models/misc/staff_scheduling.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_example_problem() -> StaffScheduling { @@ -74,7 +74,7 @@ fn test_staff_scheduling_rejects_invalid_configs() { #[test] fn test_staff_scheduling_bruteforce_solver_finds_solution() { let problem = issue_example_problem(); - let solution = BruteForce::new().find_satisfying(&problem); + let solution = BruteForce::new().find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } @@ -83,7 +83,7 @@ fn test_staff_scheduling_bruteforce_solver_finds_solution() { fn test_staff_scheduling_bruteforce_solver_detects_unsat() { let problem = StaffScheduling::new(1, vec![vec![true, false], vec![false, true]], vec![2, 2], 1); - assert!(BruteForce::new().find_satisfying(&problem).is_none()); + assert!(BruteForce::new().find_witness(&problem).is_none()); } #[test] @@ -106,7 +106,7 @@ fn test_staff_scheduling_paper_example() { let config = vec![1, 1, 1, 1, 0]; assert!(problem.evaluate(&config)); - let satisfying = BruteForce::new().find_all_satisfying(&problem); + let satisfying = BruteForce::new().find_all_witnesses(&problem); assert!(satisfying.contains(&config)); } diff --git a/src/unit_tests/models/misc/string_to_string_correction.rs b/src/unit_tests/models/misc/string_to_string_correction.rs index 9b4c25063..f4023a730 100644 --- a/src/unit_tests/models/misc/string_to_string_correction.rs +++ b/src/unit_tests/models/misc/string_to_string_correction.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -70,7 +70,7 @@ fn test_string_to_string_correction_solver() { let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1, 0], 1); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -84,7 +84,7 @@ fn test_string_to_string_correction_paper_example() { // Verify all solutions with brute force let solver = BruteForce::new(); - let all_solutions = solver.find_all_satisfying(&problem); + let all_solutions = solver.find_all_witnesses(&problem); assert!(!all_solutions.is_empty()); // The known solution must be among them assert!(all_solutions.contains(&vec![8, 5])); @@ -101,7 +101,7 @@ fn test_string_to_string_correction_unsatisfiable() { assert!(!problem.evaluate(&[])); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -127,7 +127,7 @@ fn test_string_to_string_correction_delete_only() { let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } diff --git a/src/unit_tests/models/misc/subset_sum.rs b/src/unit_tests/models/misc/subset_sum.rs index 67fb79764..906a122e9 100644 --- a/src/unit_tests/models/misc/subset_sum.rs +++ b/src/unit_tests/models/misc/subset_sum.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; use num_bigint::BigUint; @@ -76,7 +76,7 @@ fn test_subsetsum_brute_force() { let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -85,7 +85,7 @@ fn test_subsetsum_brute_force() { fn test_subsetsum_brute_force_all() { let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -97,7 +97,7 @@ fn test_subsetsum_unsatisfiable() { // Target 100 is unreachable let problem = SubsetSum::new(vec![1u32, 2, 3], 100u32); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } diff --git a/src/unit_tests/models/misc/sum_of_squares_partition.rs b/src/unit_tests/models/misc/sum_of_squares_partition.rs index f0defb70d..46230f812 100644 --- a/src/unit_tests/models/misc/sum_of_squares_partition.rs +++ b/src/unit_tests/models/misc/sum_of_squares_partition.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -80,7 +80,7 @@ fn test_sum_of_squares_partition_brute_force() { let problem = SumOfSquaresPartition::new(vec![5, 3, 8, 2, 7, 1], 3, 240); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a satisfying solution"); assert!(problem.evaluate(&solution)); } @@ -89,7 +89,7 @@ fn test_sum_of_squares_partition_brute_force() { fn test_sum_of_squares_partition_brute_force_all() { let problem = SumOfSquaresPartition::new(vec![5, 3, 8, 2, 7, 1], 3, 240); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -103,7 +103,7 @@ fn test_sum_of_squares_partition_unsatisfiable() { // Best: each element in its own group -> 100+100+100=300 > 299 let problem = SumOfSquaresPartition::new(vec![10, 10, 10], 3, 299); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -190,7 +190,7 @@ fn test_sum_of_squares_partition_paper_example() { // Brute force finds satisfying solutions let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert!(!all.is_empty()); // All solutions must have sum-of-squares <= 240 for sol in &all { diff --git a/src/unit_tests/models/misc/timetable_design.rs b/src/unit_tests/models/misc/timetable_design.rs index 01f907933..45184be81 100644 --- a/src/unit_tests/models/misc/timetable_design.rs +++ b/src/unit_tests/models/misc/timetable_design.rs @@ -1,5 +1,5 @@ use crate::models::misc::TimetableDesign; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[cfg(feature = "ilp-solver")] use std::collections::BTreeMap; @@ -124,7 +124,7 @@ fn test_timetable_design_rejects_requirement_mismatch() { #[test] fn test_timetable_design_bruteforce_solver_finds_solution() { let problem = timetable_design_toy_problem(); - let solution = BruteForce::new().find_satisfying(&problem); + let solution = BruteForce::new().find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); diff --git a/src/unit_tests/models/set/comparative_containment.rs b/src/unit_tests/models/set/comparative_containment.rs index e7e05d001..c677fd45e 100644 --- a/src/unit_tests/models/set/comparative_containment.rs +++ b/src/unit_tests/models/set/comparative_containment.rs @@ -75,11 +75,11 @@ fn test_comparative_containment_contains_selected_subset_requires_valid_config() fn test_comparative_containment_solver() { let solver = BruteForce::new(); - let yes_solutions = solver.find_all_satisfying(&yes_instance()); + let yes_solutions = solver.find_all_witnesses(&yes_instance()); assert!(yes_solutions.contains(&vec![1, 0, 0, 0])); assert!(!yes_solutions.is_empty()); - let no_solutions = solver.find_all_satisfying(&no_instance()); + let no_solutions = solver.find_all_witnesses(&no_instance()); assert!(no_solutions.is_empty()); } @@ -102,7 +102,7 @@ fn test_comparative_containment_paper_example() { assert!(problem.evaluate(&config)); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 3); assert!(solutions.contains(&config)); } diff --git a/src/unit_tests/models/set/consecutive_sets.rs b/src/unit_tests/models/set/consecutive_sets.rs index c7b86d0a4..d0f249900 100644 --- a/src/unit_tests/models/set/consecutive_sets.rs +++ b/src/unit_tests/models/set/consecutive_sets.rs @@ -39,7 +39,7 @@ fn test_consecutive_sets_no_instance() { // Search space: 4^3 = 64 configs, very fast. let problem = ConsecutiveSets::new(3, vec![vec![0, 1], vec![1, 2], vec![0, 2]], 3); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.is_empty()); } @@ -49,7 +49,7 @@ fn test_consecutive_sets_solver() { // Valid string: [0, 1, 2] — {0,1} at positions 0-1, {1,2} at positions 1-2 let problem = ConsecutiveSets::new(3, vec![vec![0, 1], vec![1, 2]], 3); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -103,7 +103,7 @@ fn test_consecutive_sets_empty_subsets() { // All unused = empty string is fine assert!(problem.evaluate(&[3, 3, 3])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); } diff --git a/src/unit_tests/models/set/exact_cover_by_3_sets.rs b/src/unit_tests/models/set/exact_cover_by_3_sets.rs index 0911ea866..f5406303b 100644 --- a/src/unit_tests/models/set/exact_cover_by_3_sets.rs +++ b/src/unit_tests/models/set/exact_cover_by_3_sets.rs @@ -64,7 +64,7 @@ fn test_exact_cover_by_3_sets_solver() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // S0={0,1,2}, S2={3,4,5}, S4={6,7,8} is an exact cover assert!(!solutions.is_empty()); @@ -83,7 +83,7 @@ fn test_exact_cover_by_3_sets_no_solution() { let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [0, 3, 4], [0, 4, 5]]); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.is_empty()); } @@ -128,7 +128,7 @@ fn test_exact_cover_by_3_sets_empty() { let problem = ExactCoverBy3Sets::new(0, vec![]); assert!(problem.evaluate(&[])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions, vec![Vec::::new()]); } diff --git a/src/unit_tests/models/set/maximum_set_packing.rs b/src/unit_tests/models/set/maximum_set_packing.rs index 9995c34fc..405d55d1b 100644 --- a/src/unit_tests/models/set/maximum_set_packing.rs +++ b/src/unit_tests/models/set/maximum_set_packing.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::Problem; +use crate::types::Max; include!("../../jl_helpers.rs"); #[test] @@ -46,17 +46,11 @@ fn test_is_set_packing_function() { assert!(is_set_packing(&sets, &[false, false, false])); // Empty is valid } -#[test] -fn test_direction() { - let problem = MaximumSetPacking::::new(vec![vec![0, 1]]); - assert_eq!(problem.direction(), Direction::Maximize); -} - #[test] fn test_empty_sets() { let problem = MaximumSetPacking::::new(vec![]); // Empty packing is valid with size 0 - assert_eq!(Problem::evaluate(&problem, &[]), SolutionSize::Valid(0)); + assert_eq!(Problem::evaluate(&problem, &[]), Max(Some(0))); } #[test] @@ -83,8 +77,8 @@ fn test_relationship_to_independent_set() { let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(&sp_problem); - let is_solutions = solver.find_all_best(&is_problem); + let sp_solutions = solver.find_all_witnesses(&sp_problem); + let is_solutions = solver.find_all_witnesses(&is_problem); // Should have same optimal value let sp_size: usize = sp_solutions[0].iter().sum(); @@ -130,7 +124,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "SetPacking best solutions mismatch"); @@ -172,6 +166,6 @@ fn test_setpacking_paper_example() { assert_eq!(result.unwrap(), 2); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 2); } diff --git a/src/unit_tests/models/set/minimum_cardinality_key.rs b/src/unit_tests/models/set/minimum_cardinality_key.rs index f97a02c00..833e3a42b 100644 --- a/src/unit_tests/models/set/minimum_cardinality_key.rs +++ b/src/unit_tests/models/set/minimum_cardinality_key.rs @@ -69,12 +69,12 @@ fn test_minimum_cardinality_key_exceeds_bound() { fn test_minimum_cardinality_key_solver() { let problem = instance1(2); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); let solution_set: HashSet> = solutions.iter().cloned().collect(); assert!(!solutions.is_empty()); assert!(solution_set.contains(&vec![1, 1, 0, 0, 0, 0])); - assert!(solutions.iter().all(|sol| problem.evaluate(sol))); + assert!(solutions.iter().all(|sol| problem.evaluate(sol).0)); } #[test] @@ -118,7 +118,7 @@ fn test_minimum_cardinality_key_empty_key_candidate() { assert!(!problem.evaluate(&[1])); let solver = BruteForce::new(); - assert_eq!(solver.find_all_satisfying(&problem), vec![vec![0]]); + assert_eq!(solver.find_all_witnesses(&problem), vec![vec![0]]); } #[test] @@ -134,9 +134,9 @@ fn test_minimum_cardinality_key_paper_example() { assert!(problem.evaluate(&solution)); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); let solution_set: HashSet> = solutions.iter().cloned().collect(); assert!(solution_set.contains(&solution)); // All returned solutions must be valid. - assert!(solutions.iter().all(|sol| problem.evaluate(sol))); + assert!(solutions.iter().all(|sol| problem.evaluate(sol).0)); } diff --git a/src/unit_tests/models/set/minimum_hitting_set.rs b/src/unit_tests/models/set/minimum_hitting_set.rs index effc8659b..576f39b44 100644 --- a/src/unit_tests/models/set/minimum_hitting_set.rs +++ b/src/unit_tests/models/set/minimum_hitting_set.rs @@ -1,8 +1,8 @@ use super::*; use crate::registry::declared_size_fields; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::Problem; +use crate::types::Min; use std::collections::HashSet; fn issue_example_problem() -> MinimumHittingSet { @@ -44,9 +44,9 @@ fn test_minimum_hitting_set_evaluate_valid_and_invalid() { assert_eq!(problem.selected_elements(&[0, 1, 0, 1]), Some(vec![1, 3])); assert_eq!(problem.selected_elements(&[0, 2, 0, 1]), None); - assert_eq!(problem.evaluate(&[0, 1, 0, 1]), SolutionSize::Valid(2)); - assert_eq!(problem.evaluate(&[1, 0, 0, 0]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[0, 2, 0, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 1, 0, 1]), Min(Some(2))); + assert_eq!(problem.evaluate(&[1, 0, 0, 0]), Min(None)); + assert_eq!(problem.evaluate(&[0, 2, 0, 1]), Min(None)); assert!(problem.is_valid_solution(&[0, 1, 0, 1])); assert!(!problem.is_valid_solution(&[1, 0, 0, 0])); assert!(!problem.is_valid_solution(&[0, 2, 0, 1])); @@ -56,8 +56,8 @@ fn test_minimum_hitting_set_evaluate_valid_and_invalid() { fn test_minimum_hitting_set_empty_set_is_always_invalid() { let problem = MinimumHittingSet::new(3, vec![vec![0, 1], vec![]]); - assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1]), Min(None)); + assert_eq!(problem.evaluate(&[0, 0, 0]), Min(None)); } #[test] @@ -78,15 +78,15 @@ fn test_minimum_hitting_set_bruteforce_optimum_issue_example() { let problem = issue_example_problem(); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&best), SolutionSize::Valid(3)); + let best = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&best), Min(Some(3))); - let best_solutions = solver.find_all_best(&problem); + let best_solutions = solver.find_all_witnesses(&problem); let best_solution_set: HashSet> = best_solutions.iter().cloned().collect(); assert!(best_solution_set.contains(&issue_example_config())); assert!(best_solutions .iter() - .all(|config| problem.evaluate(config) == SolutionSize::Valid(3))); + .all(|config| problem.evaluate(config) == Min(Some(3)))); } #[test] @@ -108,16 +108,7 @@ fn test_minimum_hitting_set_serialization_round_trip() { fn test_minimum_hitting_set_paper_example_consistency() { let problem = issue_example_problem(); - assert_eq!( - problem.evaluate(&issue_example_config()), - SolutionSize::Valid(3) - ); -} - -#[test] -fn test_minimum_hitting_set_direction() { - let problem = MinimumHittingSet::new(3, vec![vec![0, 1], vec![1, 2]]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.evaluate(&issue_example_config()), Min(Some(3))); } #[test] @@ -137,7 +128,7 @@ fn test_minimum_hitting_set_canonical_example_spec() { assert_eq!(spec.id, "minimum_hitting_set"); assert_eq!(spec.optimal_config, issue_example_config()); - assert_eq!(spec.optimal_value, serde_json::json!({"Valid": 3})); + assert_eq!(spec.optimal_value, serde_json::json!(3)); let problem: MinimumHittingSet = serde_json::from_value(spec.instance.serialize_json()).unwrap(); @@ -145,6 +136,6 @@ fn test_minimum_hitting_set_canonical_example_spec() { assert_eq!(problem.sets().len(), 7); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&best), SolutionSize::Valid(3)); + let best = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&best), Min(Some(3))); } diff --git a/src/unit_tests/models/set/minimum_set_covering.rs b/src/unit_tests/models/set/minimum_set_covering.rs index 0063bc698..c218fc56d 100644 --- a/src/unit_tests/models/set/minimum_set_covering.rs +++ b/src/unit_tests/models/set/minimum_set_covering.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::Problem; +use crate::types::Min; include!("../../jl_helpers.rs"); #[test] @@ -52,17 +52,11 @@ fn test_get_set() { assert_eq!(problem.get_set(2), None); } -#[test] -fn test_direction() { - let problem = MinimumSetCovering::::new(2, vec![vec![0, 1]]); - assert_eq!(problem.direction(), Direction::Minimize); -} - #[test] fn test_empty_universe() { let problem = MinimumSetCovering::::new(0, vec![]); // Empty universe is trivially covered with size 0 - assert_eq!(Problem::evaluate(&problem, &[]), SolutionSize::Valid(0)); + assert_eq!(Problem::evaluate(&problem, &[]), Min(Some(0))); } #[test] @@ -100,7 +94,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "SetCovering best solutions mismatch"); @@ -127,6 +121,6 @@ fn test_setcovering_paper_example() { assert_eq!(result.unwrap(), 2); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 2); } diff --git a/src/unit_tests/models/set/prime_attribute_name.rs b/src/unit_tests/models/set/prime_attribute_name.rs index 8359d4bd4..b8999597c 100644 --- a/src/unit_tests/models/set/prime_attribute_name.rs +++ b/src/unit_tests/models/set/prime_attribute_name.rs @@ -92,7 +92,7 @@ fn test_prime_attribute_name_evaluate_invalid_config() { fn test_prime_attribute_name_solver() { let problem = example1(); let solver = BruteForce::new(); - let mut solutions = solver.find_all_satisfying(&problem); + let mut solutions = solver.find_all_witnesses(&problem); solutions.sort(); assert!(!solutions.is_empty()); for sol in &solutions { @@ -108,7 +108,7 @@ fn test_prime_attribute_name_solver() { fn test_prime_attribute_name_no_solution() { let problem = example2(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.is_empty()); } diff --git a/src/unit_tests/models/set/rooted_tree_storage_assignment.rs b/src/unit_tests/models/set/rooted_tree_storage_assignment.rs index 478c71e6d..def31e1ea 100644 --- a/src/unit_tests/models/set/rooted_tree_storage_assignment.rs +++ b/src/unit_tests/models/set/rooted_tree_storage_assignment.rs @@ -42,7 +42,7 @@ fn test_rooted_tree_storage_assignment_rejects_invalid_tree_configs() { #[test] fn test_rooted_tree_storage_assignment_solver_finds_known_solution() { let problem = yes_instance(1); - let solutions = BruteForce::new().find_all_satisfying(&problem); + let solutions = BruteForce::new().find_all_witnesses(&problem); assert!(!solutions.is_empty()); assert!(solutions.contains(&vec![0, 0, 0, 1, 2])); } @@ -50,7 +50,7 @@ fn test_rooted_tree_storage_assignment_solver_finds_known_solution() { #[test] fn test_rooted_tree_storage_assignment_no_instance() { let problem = yes_instance(0); - let solutions = BruteForce::new().find_all_satisfying(&problem); + let solutions = BruteForce::new().find_all_witnesses(&problem); assert!(solutions.is_empty()); } @@ -71,6 +71,6 @@ fn test_rooted_tree_storage_assignment_paper_example() { assert!(problem.evaluate(&config)); - let solutions = BruteForce::new().find_all_satisfying(&problem); + let solutions = BruteForce::new().find_all_witnesses(&problem); assert!(solutions.contains(&config)); } diff --git a/src/unit_tests/models/set/set_basis.rs b/src/unit_tests/models/set/set_basis.rs index 375251335..ff4bb3a27 100644 --- a/src/unit_tests/models/set/set_basis.rs +++ b/src/unit_tests/models/set/set_basis.rs @@ -42,20 +42,22 @@ fn test_set_basis_no_solution_for_k_two() { assert!(!problem.evaluate(&[1, 1, 0, 0, 0, 0, 1, 0])); let solver = BruteForce::new(); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] fn test_set_basis_solver() { let problem = issue_example_problem(3); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); let solution_set: HashSet> = solutions.iter().cloned().collect(); assert_eq!(solutions.len(), 12); assert_eq!(solution_set.len(), 12); assert!(solution_set.contains(&canonical_solution())); - assert!(solutions.iter().all(|solution| problem.evaluate(solution))); + assert!(solutions + .iter() + .all(|solution| problem.evaluate(solution).0)); } #[test] @@ -78,7 +80,7 @@ fn test_set_basis_paper_example() { assert!(problem.evaluate(&solution)); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 12); } diff --git a/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs b/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs index ffbb7208e..755088d76 100644 --- a/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs +++ b/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs @@ -72,7 +72,7 @@ fn test_two_dimensional_consecutive_sets_no_instance() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.is_empty()); } @@ -82,7 +82,7 @@ fn test_two_dimensional_consecutive_sets_solver() { let problem = TwoDimensionalConsecutiveSets::new(4, vec![vec![0, 1], vec![2, 3], vec![1, 2]]); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -142,7 +142,7 @@ fn test_two_dimensional_consecutive_sets_paper_example() { // Use brute force to find all solutions let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); // The known solution should be among them assert!(solutions.contains(&valid_config)); diff --git a/src/unit_tests/property.rs b/src/unit_tests/property.rs index 537106083..b9a300871 100644 --- a/src/unit_tests/property.rs +++ b/src/unit_tests/property.rs @@ -44,8 +44,8 @@ proptest! { let vc_problem = MinimumVertexCover::new(SimpleGraph::new(n, edges), vec![1i32; n]); let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(&is_problem); - let vc_solutions = solver.find_all_best(&vc_problem); + let is_solutions = solver.find_all_witnesses(&is_problem); + let vc_solutions = solver.find_all_witnesses(&vc_problem); let is_size: usize = is_solutions[0].iter().sum(); let vc_size: usize = vc_solutions[0].iter().sum(); @@ -60,7 +60,7 @@ proptest! { let problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges), vec![1i32; n]); let solver = BruteForce::new(); - for sol in solver.find_all_best(&problem) { + for sol in solver.find_all_witnesses(&problem) { // Any subset of an IS is also an IS for i in 0..n { let mut subset = sol.clone(); @@ -77,7 +77,7 @@ proptest! { let problem = MinimumVertexCover::new(SimpleGraph::new(n, edges), vec![1i32; n]); let solver = BruteForce::new(); - for sol in solver.find_all_best(&problem) { + for sol in solver.find_all_witnesses(&problem) { // Adding any vertex to a VC still gives a valid VC for i in 0..n { let mut superset = sol.clone(); @@ -96,7 +96,7 @@ proptest! { let solver = BruteForce::new(); // Get all valid independent sets (not just optimal) - for sol in solver.find_all_best(&is_problem) { + for sol in solver.find_all_witnesses(&is_problem) { // The complement should be a valid vertex cover let complement: Vec = sol.iter().map(|&x| 1 - x).collect(); prop_assert!(vc_problem.evaluate(&complement).is_valid(), @@ -129,12 +129,12 @@ proptest! { let problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges), vec![1i32; n]); let solver = BruteForce::new(); - for sol in solver.find_all_best(&problem) { + for sol in solver.find_all_witnesses(&problem) { let metric = problem.evaluate(&sol); // Valid solutions have non-negative size prop_assert!(metric.is_valid()); - if let crate::types::SolutionSize::Valid(size) = metric { - prop_assert!(size >= 0); + if let Some(size) = metric.size() { + prop_assert!(*size >= 0); } } } diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index dc636116e..ff9a14967 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -2,8 +2,8 @@ use crate::models::formula::KSatisfiability; use crate::prelude::*; -use crate::rules::{MinimizeSteps, ReductionGraph, TraversalDirection}; -use crate::topology::{SimpleGraph, TriangularSubgraph}; +use crate::rules::{MinimizeSteps, ReductionGraph, ReductionMode, TraversalFlow}; +use crate::topology::{KingsSubgraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph}; use crate::types::ProblemSize; use crate::variant::K3; use std::collections::BTreeMap; @@ -30,19 +30,6 @@ fn test_reduction_graph_discovers_registered_reductions() { assert!(graph.has_direct_reduction_by_name("Satisfiability", "MaximumIndependentSet")); } -#[test] -fn test_bidirectional_reductions() { - let graph = ReductionGraph::new(); - - // IS <-> VC should both be registered - assert!(graph.has_direct_reduction_by_name("MaximumIndependentSet", "MinimumVertexCover")); - assert!(graph.has_direct_reduction_by_name("MinimumVertexCover", "MaximumIndependentSet")); - - // MaxCut <-> SpinGlass should both be registered - assert!(graph.has_direct_reduction_by_name("MaxCut", "SpinGlass")); - assert!(graph.has_direct_reduction_by_name("SpinGlass", "MaxCut")); -} - // ---- Path finding (by name) ---- #[test] @@ -97,6 +84,68 @@ fn test_multi_step_path() { ); } +#[test] +fn aggregate_mode_rejects_witness_only_real_edge() { + let graph = ReductionGraph::new(); + let src = ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); + let dst = ReductionGraph::variant_to_map(&MinimumVertexCover::::variant()); + + assert!(graph + .find_cheapest_path_mode( + "MaximumIndependentSet", + &src, + "MinimumVertexCover", + &dst, + ReductionMode::Witness, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_some()); + assert!(graph + .find_cheapest_path_mode( + "MaximumIndependentSet", + &src, + "MinimumVertexCover", + &dst, + ReductionMode::Aggregate, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_none()); +} + +#[test] +fn natural_edge_supports_both_modes_public_api() { + let graph = ReductionGraph::new(); + let src = + ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); + let dst = + ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); + + assert!(graph + .find_cheapest_path_mode( + "MaximumIndependentSet", + &src, + "MaximumIndependentSet", + &dst, + ReductionMode::Witness, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_some()); + assert!(graph + .find_cheapest_path_mode( + "MaximumIndependentSet", + &src, + "MaximumIndependentSet", + &dst, + ReductionMode::Aggregate, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_some()); +} + #[test] fn test_problem_size_propagation() { let graph = ReductionGraph::new(); @@ -206,40 +255,6 @@ fn test_no_path_exists() { assert!(paths.is_empty()); } -#[test] -fn test_bidirectional_paths() { - let graph = ReductionGraph::new(); - let is_var = - ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); - let vc_var = ReductionGraph::variant_to_map(&MinimumVertexCover::::variant()); - let sg_var = ReductionGraph::variant_to_map(&SpinGlass::::variant()); - let qubo_var = ReductionGraph::variant_to_map(&QUBO::::variant()); - - assert!(!graph - .find_all_paths( - "MaximumIndependentSet", - &is_var, - "MinimumVertexCover", - &vc_var - ) - .is_empty()); - assert!(!graph - .find_all_paths( - "MinimumVertexCover", - &vc_var, - "MaximumIndependentSet", - &is_var - ) - .is_empty()); - - assert!(!graph - .find_all_paths("SpinGlass", &sg_var, "QUBO", &qubo_var) - .is_empty()); - assert!(!graph - .find_all_paths("QUBO", &qubo_var, "SpinGlass", &sg_var) - .is_empty()); -} - // ---- Display ---- #[test] @@ -385,7 +400,7 @@ fn test_k_neighbors_outgoing() { "MaximumIndependentSet", default_variant, 1, - TraversalDirection::Outgoing, + TraversalFlow::Outgoing, ); assert!(!neighbors.is_empty()); assert!(neighbors.iter().all(|n| n.hops == 1)); @@ -395,7 +410,7 @@ fn test_k_neighbors_outgoing() { "MaximumIndependentSet", default_variant, 2, - TraversalDirection::Outgoing, + TraversalFlow::Outgoing, ); assert!(neighbors_2.len() >= neighbors.len()); } @@ -406,7 +421,7 @@ fn test_k_neighbors_incoming() { let variants = graph.variants_for("QUBO"); assert!(!variants.is_empty()); - let neighbors = graph.k_neighbors("QUBO", &variants[0], 1, TraversalDirection::Incoming); + let neighbors = graph.k_neighbors("QUBO", &variants[0], 1, TraversalFlow::Incoming); // QUBO is a common target — should have incoming reductions assert!(!neighbors.is_empty()); } @@ -421,19 +436,19 @@ fn test_k_neighbors_both() { "MaximumIndependentSet", default_variant, 1, - TraversalDirection::Outgoing, + TraversalFlow::Outgoing, ); let in_only = graph.k_neighbors( "MaximumIndependentSet", default_variant, 1, - TraversalDirection::Incoming, + TraversalFlow::Incoming, ); let both = graph.k_neighbors( "MaximumIndependentSet", default_variant, 1, - TraversalDirection::Both, + TraversalFlow::Both, ); // Both should be >= max of either direction assert!(both.len() >= out_only.len()); @@ -444,7 +459,7 @@ fn test_k_neighbors_both() { fn test_k_neighbors_unknown_problem() { let graph = ReductionGraph::new(); let empty = BTreeMap::new(); - let neighbors = graph.k_neighbors("NonExistent", &empty, 2, TraversalDirection::Outgoing); + let neighbors = graph.k_neighbors("NonExistent", &empty, 2, TraversalFlow::Outgoing); assert!(neighbors.is_empty()); } @@ -457,7 +472,7 @@ fn test_k_neighbors_zero_hops() { "MaximumIndependentSet", default_variant, 0, - TraversalDirection::Outgoing, + TraversalFlow::Outgoing, ); assert!(neighbors.is_empty()); } @@ -649,3 +664,65 @@ fn find_best_entry_accepts_exact_source_and_target_variant() { "Should find exact match on both source and target variant" ); } + +#[test] +fn test_has_direct_reduction_mode_witness() { + let graph = ReductionGraph::new(); + + // MIS -> MVC is witness-only, so Witness mode should find it + assert!(graph + .has_direct_reduction_mode::, MinimumVertexCover>( + ReductionMode::Witness, + )); +} + +#[test] +fn test_has_direct_reduction_by_name_mode() { + let graph = ReductionGraph::new(); + + assert!(graph.has_direct_reduction_by_name_mode( + "MaximumIndependentSet", + "MinimumVertexCover", + ReductionMode::Witness, + )); + + // Aggregate mode should not find witness-only edges + assert!(!graph.has_direct_reduction_by_name_mode( + "MaximumIndependentSet", + "MinimumVertexCover", + ReductionMode::Aggregate, + )); +} + +#[test] +fn test_find_all_paths_mode_witness() { + let graph = ReductionGraph::new(); + let src = ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); + let dst = ReductionGraph::variant_to_map(&MinimumVertexCover::::variant()); + + let paths = graph.find_all_paths_mode( + "MaximumIndependentSet", + &src, + "MinimumVertexCover", + &dst, + ReductionMode::Witness, + ); + assert!(!paths.is_empty()); +} + +#[test] +fn test_find_all_paths_mode_aggregate_rejects_witness_only() { + let graph = ReductionGraph::new(); + let src = ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); + let dst = ReductionGraph::variant_to_map(&MinimumVertexCover::::variant()); + + // MIS -> MVC is witness-only, so aggregate mode should find no paths + let paths = graph.find_all_paths_mode( + "MaximumIndependentSet", + &src, + "MinimumVertexCover", + &dst, + ReductionMode::Aggregate, + ); + assert!(paths.is_empty()); +} diff --git a/src/unit_tests/registry/dispatch.rs b/src/unit_tests/registry/dispatch.rs index bb8889c77..471d6f957 100644 --- a/src/unit_tests/registry/dispatch.rs +++ b/src/unit_tests/registry/dispatch.rs @@ -1,19 +1,65 @@ use crate::models::graph::MaximumIndependentSet; +use crate::models::graph::MinimumVertexCover; use crate::models::misc::SubsetSum; use crate::registry::variant::find_variant_entry; use crate::registry::{load_dyn, serialize_any, DynProblem, LoadedDynProblem}; use crate::topology::SimpleGraph; +use crate::types::Sum; use crate::{Problem, Solver}; use std::any::Any; use std::collections::BTreeMap; -fn solve_subset_sum(any: &dyn Any) -> Option<(Vec, String)> { +fn solve_subset_sum_value(any: &dyn Any) -> String { + let p = any.downcast_ref::().unwrap(); + if let Some(config) = crate::BruteForce::new().find_witness(p) { + format!("{:?}", p.evaluate(&config)) + } else { + "false".to_string() + } +} + +fn solve_subset_sum_witness(any: &dyn Any) -> Option<(Vec, String)> { let p = any.downcast_ref::()?; - let config = crate::BruteForce::new().find_satisfying(p)?; + let config = crate::BruteForce::new().find_witness(p)?; let eval = format!("{:?}", p.evaluate(&config)); Some((config, eval)) } +#[derive(Clone, serde::Serialize)] +struct AggregateOnlyProblem { + weights: Vec, +} + +impl Problem for AggregateOnlyProblem { + const NAME: &'static str = "AggregateOnlyProblem"; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![2; self.weights.len()] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config + .iter() + .zip(&self.weights) + .map(|(&c, &w)| if c == 1 { w } else { 0 }) + .sum()) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +fn solve_aggregate_value(any: &dyn Any) -> String { + let p = any.downcast_ref::().unwrap(); + format!("{:?}", crate::BruteForce::new().solve(p)) +} + +fn solve_aggregate_witness(_: &dyn Any) -> Option<(Vec, String)> { + None +} + #[test] fn test_dyn_problem_blanket_impl_exposes_problem_metadata() { let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); @@ -27,16 +73,64 @@ fn test_dyn_problem_blanket_impl_exposes_problem_metadata() { } #[test] -fn test_loaded_dyn_problem_delegates_to_solve_fn() { +fn test_dyn_problem_formats_optimization_values_as_max_min() { + let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); + let dyn_problem: &dyn DynProblem = &problem; + + assert_eq!(dyn_problem.evaluate_dyn(&[1, 0, 1]), "Max(2)"); + assert_eq!(dyn_problem.evaluate_dyn(&[1, 1, 0]), "Max(None)"); +} + +#[test] +fn test_loaded_dyn_problem_delegates_to_value_and_witness_fns() { let problem = SubsetSum::new(vec![3u32, 7u32, 1u32], 4u32); - let loaded = LoadedDynProblem::new(Box::new(problem), solve_subset_sum); + let loaded = LoadedDynProblem::new( + Box::new(problem), + solve_subset_sum_value, + solve_subset_sum_witness, + ); + + assert_eq!(loaded.solve_brute_force_value(), "Or(true)"); let solved = loaded - .solve_brute_force() + .solve_brute_force_witness() .expect("expected satisfying solution"); - assert_eq!(solved.1, "true"); + assert_eq!(solved.1, "Or(true)"); assert_eq!(solved.0.len(), 3); } +#[test] +fn loaded_dyn_problem_returns_none_for_aggregate_only_witness() { + let loaded = LoadedDynProblem::new( + Box::new(AggregateOnlyProblem { + weights: vec![1, 2, 4], + }), + solve_aggregate_value, + solve_aggregate_witness, + ); + + assert_eq!(loaded.solve_brute_force_value(), "Sum(28)"); + assert!(loaded.solve_brute_force_witness().is_none()); +} + +#[test] +fn test_load_dyn_formats_optimization_solve_values_as_max_min() { + let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); + let variant = BTreeMap::from([ + ("graph".to_string(), "SimpleGraph".to_string()), + ("weight".to_string(), "i32".to_string()), + ]); + let loaded = load_dyn( + "MinimumVertexCover", + &variant, + serde_json::to_value(&problem).unwrap(), + ) + .unwrap(); + + assert_eq!(loaded.solve_brute_force_value(), "Min(1)"); + let solved = loaded.solve_brute_force_witness().unwrap(); + assert_eq!(solved.1, "Min(1)"); +} + #[test] fn test_find_variant_entry_requires_exact_variant() { let partial = BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]); @@ -62,7 +156,8 @@ fn test_load_dyn_round_trips_maximum_independent_set() { loaded.serialize_json(), serde_json::to_value(&problem).unwrap() ); - assert!(loaded.solve_brute_force().is_some()); + assert!(!loaded.solve_brute_force_value().is_empty()); + assert!(loaded.solve_brute_force_witness().is_some()); } #[test] @@ -75,8 +170,10 @@ fn test_load_dyn_solves_subset_sum() { serde_json::to_value(&problem).unwrap(), ) .unwrap(); - let solved = loaded.solve_brute_force().unwrap(); - assert_eq!(solved.1, "true"); + + assert_eq!(loaded.solve_brute_force_value(), "Or(true)"); + let solved = loaded.solve_brute_force_witness().unwrap(); + assert_eq!(solved.1, "Or(true)"); } #[test] @@ -120,3 +217,53 @@ fn test_serialize_any_rejects_partial_variant() { let partial = BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]); assert!(serialize_any("MaximumIndependentSet", &partial, &problem as &dyn Any).is_none()); } + +#[test] +fn test_format_metric_uses_display() { + use crate::registry::dyn_problem::format_metric; + use crate::types::{Max, Min, Or}; + assert_eq!(format_metric(&Max(Some(42))), "Max(42)"); + assert_eq!(format_metric(&Max::(None)), "Max(None)"); + assert_eq!(format_metric(&Min(Some(7))), "Min(7)"); + assert_eq!(format_metric(&Or(true)), "Or(true)"); + assert_eq!(format_metric(&Sum(99u64)), "Sum(99)"); +} + +#[test] +fn test_loaded_dyn_problem_backward_compat_solve() { + let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); + let variant = BTreeMap::from([ + ("graph".to_string(), "SimpleGraph".to_string()), + ("weight".to_string(), "i32".to_string()), + ]); + let loaded = load_dyn( + "MaximumIndependentSet", + &variant, + serde_json::to_value(&problem).unwrap(), + ) + .unwrap(); + // solve_brute_force() is the backward-compatible alias for solve_brute_force_witness() + let result = loaded.solve_brute_force(); + assert!(result.is_some()); + let (config, eval) = result.unwrap(); + assert!(!config.is_empty()); + assert!(eval.starts_with("Max(")); +} + +#[test] +fn test_loaded_dyn_problem_debug() { + let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); + let variant = BTreeMap::from([ + ("graph".to_string(), "SimpleGraph".to_string()), + ("weight".to_string(), "i32".to_string()), + ]); + let loaded = load_dyn( + "MaximumIndependentSet", + &variant, + serde_json::to_value(&problem).unwrap(), + ) + .unwrap(); + let debug = format!("{:?}", loaded); + assert!(debug.contains("LoadedDynProblem")); + assert!(debug.contains("MaximumIndependentSet")); +} diff --git a/src/unit_tests/rules/binpacking_ilp.rs b/src/unit_tests/rules/binpacking_ilp.rs index 87633735f..b7b63cf38 100644 --- a/src/unit_tests/rules/binpacking_ilp.rs +++ b/src/unit_tests/rules/binpacking_ilp.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; #[test] fn test_reduction_creates_valid_ilp() { @@ -29,7 +29,7 @@ fn test_binpacking_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); // Solve original with brute force - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_obj = problem.evaluate(&bf_solutions[0]); // Solve via ILP @@ -37,8 +37,8 @@ fn test_binpacking_to_ilp_closed_loop() { let extracted = reduction.extract_solution(&ilp_solution); let ilp_obj = problem.evaluate(&extracted); - assert_eq!(bf_obj, SolutionSize::Valid(2)); - assert_eq!(ilp_obj, SolutionSize::Valid(2)); + assert_eq!(bf_obj, Min(Some(2))); + assert_eq!(ilp_obj, Min(Some(2))); } #[test] @@ -55,7 +55,7 @@ fn test_single_item() { let extracted = reduction.extract_solution(&ilp_solution); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); } #[test] @@ -70,7 +70,7 @@ fn test_same_weight_items() { let extracted = reduction.extract_solution(&ilp_solution); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&extracted), Min(Some(2))); } #[test] @@ -85,7 +85,7 @@ fn test_exact_fill() { let extracted = reduction.extract_solution(&ilp_solution); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); } #[test] @@ -139,5 +139,5 @@ fn test_solve_reduced() { .expect("solve_reduced should work"); assert!(problem.evaluate(&solution).is_valid()); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(&solution), Min(Some(3))); } diff --git a/src/unit_tests/rules/circuit_ilp.rs b/src/unit_tests/rules/circuit_ilp.rs index b40b5cc92..60ed7e176 100644 --- a/src/unit_tests/rules/circuit_ilp.rs +++ b/src/unit_tests/rules/circuit_ilp.rs @@ -49,7 +49,7 @@ fn test_circuitsat_to_ilp_xor_gate() { &reduction, "CircuitSAT->ILP XOR gate", ); - assert_eq!(BruteForce::new().find_all_satisfying(&source).len(), 4); + assert_eq!(BruteForce::new().find_all_witnesses(&source).len(), 4); } #[test] diff --git a/src/unit_tests/rules/circuit_spinglass.rs b/src/unit_tests/rules/circuit_spinglass.rs index d1b43c5a1..e648499a3 100644 --- a/src/unit_tests/rules/circuit_spinglass.rs +++ b/src/unit_tests/rules/circuit_spinglass.rs @@ -19,9 +19,10 @@ where + std::ops::Mul + std::fmt::Debug + NumericSize, + ::Sum: std::fmt::Debug + serde::Serialize + serde::de::DeserializeOwned, { let solver = BruteForce::new(); - let solutions = solver.find_all_best(&gadget.problem); + let solutions = solver.find_all_witnesses(&gadget.problem); // For each expected input/output pair, verify there's a matching ground state for (inputs, outputs) in expected { @@ -120,7 +121,7 @@ fn test_set0_gadget() { assert_eq!(gadget.outputs, vec![0]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&gadget.problem); + let solutions = solver.find_all_witnesses(&gadget.problem); // Ground state should be spin down (0) assert!(solutions.contains(&vec![0])); assert!(!solutions.contains(&vec![1])); @@ -134,7 +135,7 @@ fn test_set1_gadget() { assert_eq!(gadget.outputs, vec![0]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&gadget.problem); + let solutions = solver.find_all_witnesses(&gadget.problem); // Ground state should be spin up (1) assert!(solutions.contains(&vec![1])); assert!(!solutions.contains(&vec![0])); @@ -152,7 +153,7 @@ fn test_constant_true() { let sg = reduction.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(sg); + let solutions = solver.find_all_witnesses(sg); let extracted: Vec> = solutions .iter() @@ -179,7 +180,7 @@ fn test_constant_false() { let sg = reduction.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(sg); + let solutions = solver.find_all_witnesses(sg); let extracted: Vec> = solutions .iter() @@ -210,7 +211,7 @@ fn test_multi_input_and() { let sg = reduction.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(sg); + let solutions = solver.find_all_witnesses(sg); let extracted: Vec> = solutions .iter() diff --git a/src/unit_tests/rules/closestvectorproblem_qubo.rs b/src/unit_tests/rules/closestvectorproblem_qubo.rs index c1209d227..90938593d 100644 --- a/src/unit_tests/rules/closestvectorproblem_qubo.rs +++ b/src/unit_tests/rules/closestvectorproblem_qubo.rs @@ -75,7 +75,7 @@ fn test_duplicate_target_encodings_have_equal_qubo_value() { let reduction = ReduceTo::>::reduce_to(&canonical_cvp()); let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let best = solver.find_all_best(qubo); + let best = solver.find_all_witnesses(qubo); assert!(best.contains(&vec![0, 0, 1, 0, 0, 1]) || best.contains(&vec![1, 1, 0, 1, 1, 0])); assert_close( diff --git a/src/unit_tests/rules/coloring_ilp.rs b/src/unit_tests/rules/coloring_ilp.rs index 68e83d7af..f9cbfa0b9 100644 --- a/src/unit_tests/rules/coloring_ilp.rs +++ b/src/unit_tests/rules/coloring_ilp.rs @@ -53,8 +53,8 @@ fn test_coloring_to_ilp_closed_loop() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - // Solve with brute force on original problem - use find_all_satisfying for satisfaction problems - let bf_solutions = bf.find_all_satisfying(&problem); + // Solve with brute force on original problem - use find_all_witnesses for satisfaction problems + let bf_solutions = bf.find_all_witnesses(&problem); assert!( !bf_solutions.is_empty(), "Brute force should find solutions" diff --git a/src/unit_tests/rules/coloring_qubo.rs b/src/unit_tests/rules/coloring_qubo.rs index 54ba3177c..daa61681a 100644 --- a/src/unit_tests/rules/coloring_qubo.rs +++ b/src/unit_tests/rules/coloring_qubo.rs @@ -11,7 +11,7 @@ fn test_kcoloring_to_qubo_closed_loop() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // All solutions should extract to valid colorings for sol in &qubo_solutions { @@ -31,7 +31,7 @@ fn test_kcoloring_to_qubo_path() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -51,7 +51,7 @@ fn test_kcoloring_to_qubo_reversed_edges() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); diff --git a/src/unit_tests/rules/factoring_circuit.rs b/src/unit_tests/rules/factoring_circuit.rs index 9b4345e04..5cea21a14 100644 --- a/src/unit_tests/rules/factoring_circuit.rs +++ b/src/unit_tests/rules/factoring_circuit.rs @@ -311,7 +311,7 @@ fn test_jl_parity_factoring_to_circuitsat() { .unwrap(); let solver = BruteForce::new(); let jl_best_source = jl_parse_configs_set(&data["cases"][0]["best_source"]); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_eq!( best_source, jl_best_source, "Factoring best source mismatch" diff --git a/src/unit_tests/rules/factoring_ilp.rs b/src/unit_tests/rules/factoring_ilp.rs index 61b10a0c9..06f1fd7bf 100644 --- a/src/unit_tests/rules/factoring_ilp.rs +++ b/src/unit_tests/rules/factoring_ilp.rs @@ -177,7 +177,7 @@ fn test_factoring_to_ilp_closed_loop() { // Get brute force solutions let bf = BruteForce::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); // ILP solution should be among brute force solutions let (a, b) = problem.read_factors(&ilp_factors); diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index a7c5d9326..ee1fb0933 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -4,12 +4,229 @@ use crate::models::graph::{MaximumIndependentSet, MinimumVertexCover}; use crate::models::misc::Knapsack; use crate::models::set::MaximumSetPacking; use crate::rules::cost::{Minimize, MinimizeSteps}; -use crate::rules::graph::{classify_problem_category, ReductionStep}; -use crate::rules::registry::ReductionEntry; +use crate::rules::graph::{classify_problem_category, ReductionMode, ReductionStep}; +use crate::rules::registry::{EdgeCapabilities, ReductionEntry}; +use crate::rules::traits::{AggregateReductionResult, ReductionResult}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::{One, ProblemSize}; -use std::collections::BTreeMap; +use crate::types::{One, ProblemSize, Sum}; +use petgraph::graph::DiGraph; +use serde_json::json; +use std::any::Any; +use std::collections::{BTreeMap, HashMap}; + +#[derive(Clone)] +struct AggregateChainSource; + +#[derive(Clone)] +struct AggregateChainMiddle; + +#[derive(Clone)] +struct AggregateChainTarget; + +#[derive(Clone)] +struct NaturalVariantProblem; + +impl Problem for AggregateChainSource { + const NAME: &'static str = "AggregateChainSource"; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![1] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config.iter().sum::() as u64) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +impl Problem for AggregateChainMiddle { + const NAME: &'static str = "AggregateChainMiddle"; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![1] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config.iter().sum::() as u64) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +impl Problem for AggregateChainTarget { + const NAME: &'static str = "AggregateChainTarget"; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![1] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config.iter().sum::() as u64) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +impl Problem for NaturalVariantProblem { + const NAME: &'static str = "NaturalVariantProblem"; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![1] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config.iter().sum::() as u64) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +struct SourceToMiddleAggregateResult { + target: AggregateChainMiddle, +} + +impl AggregateReductionResult for SourceToMiddleAggregateResult { + type Source = AggregateChainSource; + type Target = AggregateChainMiddle; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, target_value: Sum) -> Sum { + Sum(target_value.0 + 2) + } +} + +struct MiddleToTargetAggregateResult { + target: AggregateChainTarget, +} + +impl AggregateReductionResult for MiddleToTargetAggregateResult { + type Source = AggregateChainMiddle; + type Target = AggregateChainTarget; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, target_value: Sum) -> Sum { + Sum(target_value.0 + 3) + } +} + +fn reduce_source_to_middle_aggregate( + any: &dyn Any, +) -> Box { + any.downcast_ref::() + .expect("expected AggregateChainSource"); + Box::new(SourceToMiddleAggregateResult { + target: AggregateChainMiddle, + }) +} + +fn reduce_middle_to_target_aggregate( + any: &dyn Any, +) -> Box { + any.downcast_ref::() + .expect("expected AggregateChainMiddle"); + Box::new(MiddleToTargetAggregateResult { + target: AggregateChainTarget, + }) +} + +struct SourceToMiddleWitnessResult { + target: AggregateChainMiddle, +} + +impl ReductionResult for SourceToMiddleWitnessResult { + type Source = AggregateChainSource; + type Target = AggregateChainMiddle; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_solution(&self, target_solution: &[usize]) -> Vec { + target_solution.to_vec() + } +} + +fn reduce_source_to_middle_witness( + any: &dyn Any, +) -> Box { + any.downcast_ref::() + .expect("expected AggregateChainSource"); + Box::new(SourceToMiddleWitnessResult { + target: AggregateChainMiddle, + }) +} + +fn reduce_natural_variant_witness( + any: &dyn Any, +) -> Box { + let source = any + .downcast_ref::() + .expect("expected NaturalVariantProblem"); + Box::new(crate::rules::ReductionAutoCast::< + NaturalVariantProblem, + NaturalVariantProblem, + >::new(source.clone())) +} + +fn build_two_node_graph( + source_name: &'static str, + source_variant: BTreeMap, + target_name: &'static str, + target_variant: BTreeMap, + edge: ReductionEdgeData, +) -> ReductionGraph { + let nodes = vec![ + VariantNode { + name: source_name, + variant: source_variant.clone(), + complexity: "", + }, + VariantNode { + name: target_name, + variant: target_variant.clone(), + complexity: "", + }, + ]; + + let mut graph = DiGraph::new(); + let source_idx = graph.add_node(0); + let target_idx = graph.add_node(1); + graph.add_edge(source_idx, target_idx, edge); + + let mut name_to_nodes = HashMap::new(); + name_to_nodes.insert(source_name, vec![source_idx]); + name_to_nodes + .entry(target_name) + .or_insert_with(Vec::new) + .push(target_idx); + + ReductionGraph { + graph, + nodes, + name_to_nodes, + default_variants: HashMap::new(), + } +} #[test] fn test_find_direct_path() { @@ -24,6 +241,279 @@ fn test_find_direct_path() { assert_eq!(shortest.len(), 1); // One reduction step } +#[test] +fn test_aggregate_reduction_chain_extracts_value_backwards() { + let source_variant = BTreeMap::new(); + let middle_variant = BTreeMap::new(); + let target_variant = BTreeMap::new(); + + let nodes = vec![ + VariantNode { + name: AggregateChainSource::NAME, + variant: source_variant.clone(), + complexity: "", + }, + VariantNode { + name: AggregateChainMiddle::NAME, + variant: middle_variant.clone(), + complexity: "", + }, + VariantNode { + name: AggregateChainTarget::NAME, + variant: target_variant.clone(), + complexity: "", + }, + ]; + + let mut graph = DiGraph::new(); + let source_idx = graph.add_node(0); + let middle_idx = graph.add_node(1); + let target_idx = graph.add_node(2); + + graph.add_edge( + source_idx, + middle_idx, + ReductionEdgeData { + overhead: crate::rules::registry::ReductionOverhead::default(), + reduce_fn: None, + reduce_aggregate_fn: Some(reduce_source_to_middle_aggregate), + capabilities: EdgeCapabilities::aggregate_only(), + }, + ); + graph.add_edge( + middle_idx, + target_idx, + ReductionEdgeData { + overhead: crate::rules::registry::ReductionOverhead::default(), + reduce_fn: None, + reduce_aggregate_fn: Some(reduce_middle_to_target_aggregate), + capabilities: EdgeCapabilities::aggregate_only(), + }, + ); + + let reduction_graph = ReductionGraph { + graph, + nodes, + name_to_nodes: HashMap::from([ + (AggregateChainSource::NAME, vec![source_idx]), + (AggregateChainMiddle::NAME, vec![middle_idx]), + (AggregateChainTarget::NAME, vec![target_idx]), + ]), + default_variants: HashMap::new(), + }; + let path = ReductionPath { + steps: vec![ + ReductionStep { + name: AggregateChainSource::NAME.to_string(), + variant: source_variant, + }, + ReductionStep { + name: AggregateChainMiddle::NAME.to_string(), + variant: middle_variant, + }, + ReductionStep { + name: AggregateChainTarget::NAME.to_string(), + variant: target_variant, + }, + ], + }; + + let chain = reduction_graph + .reduce_aggregate_along_path(&path, &AggregateChainSource as &dyn Any) + .expect("expected aggregate reduction chain"); + + assert_eq!( + chain.target_problem::().dims(), + vec![1] + ); + assert_eq!(chain.extract_value_dyn(json!(7)), json!(12)); +} + +#[test] +fn witness_path_search_rejects_aggregate_only_edge() { + let source_variant = BTreeMap::new(); + let target_variant = BTreeMap::new(); + let graph = build_two_node_graph( + AggregateChainSource::NAME, + source_variant.clone(), + AggregateChainMiddle::NAME, + target_variant.clone(), + ReductionEdgeData { + overhead: crate::rules::registry::ReductionOverhead::default(), + reduce_fn: None, + reduce_aggregate_fn: Some(reduce_source_to_middle_aggregate), + capabilities: EdgeCapabilities::aggregate_only(), + }, + ); + + assert!(graph + .find_cheapest_path_mode( + AggregateChainSource::NAME, + &source_variant, + AggregateChainMiddle::NAME, + &target_variant, + ReductionMode::Witness, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_none()); + assert!(graph + .find_cheapest_path_mode( + AggregateChainSource::NAME, + &source_variant, + AggregateChainMiddle::NAME, + &target_variant, + ReductionMode::Aggregate, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_some()); +} + +#[test] +fn aggregate_path_search_rejects_witness_only_edge() { + let source_variant = BTreeMap::new(); + let target_variant = BTreeMap::new(); + let graph = build_two_node_graph( + AggregateChainSource::NAME, + source_variant.clone(), + AggregateChainMiddle::NAME, + target_variant.clone(), + ReductionEdgeData { + overhead: crate::rules::registry::ReductionOverhead::default(), + reduce_fn: Some(reduce_source_to_middle_witness), + reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), + }, + ); + + assert!(graph + .find_cheapest_path_mode( + AggregateChainSource::NAME, + &source_variant, + AggregateChainMiddle::NAME, + &target_variant, + ReductionMode::Aggregate, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_none()); + assert!(graph + .find_cheapest_path_mode( + AggregateChainSource::NAME, + &source_variant, + AggregateChainMiddle::NAME, + &target_variant, + ReductionMode::Witness, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_some()); +} + +#[test] +fn natural_edge_supports_both_modes() { + let source_variant = BTreeMap::from([("graph".to_string(), "Source".to_string())]); + let target_variant = BTreeMap::from([("graph".to_string(), "Target".to_string())]); + let graph = build_two_node_graph( + NaturalVariantProblem::NAME, + source_variant.clone(), + NaturalVariantProblem::NAME, + target_variant.clone(), + ReductionEdgeData { + overhead: crate::rules::registry::ReductionOverhead::default(), + reduce_fn: Some(reduce_natural_variant_witness), + reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::both(), + }, + ); + + let witness_path = graph.find_cheapest_path_mode( + NaturalVariantProblem::NAME, + &source_variant, + NaturalVariantProblem::NAME, + &target_variant, + ReductionMode::Witness, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ); + let aggregate_path = graph.find_cheapest_path_mode( + NaturalVariantProblem::NAME, + &source_variant, + NaturalVariantProblem::NAME, + &target_variant, + ReductionMode::Aggregate, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ); + + assert!(witness_path.is_some()); + let aggregate_path = aggregate_path.expect("expected aggregate path"); + let chain = graph + .reduce_aggregate_along_path(&aggregate_path, &NaturalVariantProblem as &dyn Any) + .expect("expected aggregate chain"); + assert_eq!(chain.extract_value_dyn(json!(7)), json!(7)); +} + +#[test] +fn reduce_aggregate_along_path_rejects_single_step_path() { + let source_variant = BTreeMap::new(); + let graph = build_two_node_graph( + AggregateChainSource::NAME, + source_variant.clone(), + AggregateChainMiddle::NAME, + BTreeMap::new(), + ReductionEdgeData { + overhead: crate::rules::registry::ReductionOverhead::default(), + reduce_fn: None, + reduce_aggregate_fn: Some(reduce_source_to_middle_aggregate), + capabilities: EdgeCapabilities::aggregate_only(), + }, + ); + let single_step_path = ReductionPath { + steps: vec![ReductionStep { + name: AggregateChainSource::NAME.to_string(), + variant: source_variant, + }], + }; + assert!(graph + .reduce_aggregate_along_path(&single_step_path, &AggregateChainSource as &dyn Any) + .is_none()); +} + +#[test] +fn reduce_aggregate_returns_none_for_witness_only_edge() { + let source_variant = BTreeMap::new(); + let target_variant = BTreeMap::new(); + let graph = build_two_node_graph( + AggregateChainSource::NAME, + source_variant.clone(), + AggregateChainMiddle::NAME, + target_variant.clone(), + ReductionEdgeData { + overhead: crate::rules::registry::ReductionOverhead::default(), + reduce_fn: Some(reduce_source_to_middle_witness), + reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), + }, + ); + let path = ReductionPath { + steps: vec![ + ReductionStep { + name: AggregateChainSource::NAME.to_string(), + variant: source_variant, + }, + ReductionStep { + name: AggregateChainMiddle::NAME.to_string(), + variant: target_variant, + }, + ], + }; + assert!(graph + .reduce_aggregate_along_path(&path, &AggregateChainSource as &dyn Any) + .is_none()); +} + #[test] fn test_find_indirect_path() { let graph = ReductionGraph::new(); @@ -206,32 +696,6 @@ fn test_reduction_path_methods() { assert!(path.target().unwrap().contains("MinimumVertexCover")); } -#[test] -fn test_bidirectional_paths() { - let graph = ReductionGraph::new(); - let is_var = - ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); - let vc_var = ReductionGraph::variant_to_map(&MinimumVertexCover::::variant()); - - // Forward path - let forward = graph.find_all_paths( - "MaximumIndependentSet", - &is_var, - "MinimumVertexCover", - &vc_var, - ); - assert!(!forward.is_empty()); - - // Backward path - let backward = graph.find_all_paths( - "MinimumVertexCover", - &vc_var, - "MaximumIndependentSet", - &is_var, - ); - assert!(!backward.is_empty()); -} - #[test] fn test_to_json() { let graph = ReductionGraph::new(); @@ -852,7 +1316,7 @@ fn test_reduce_along_path_direct() { #[test] fn test_reduction_chain_direct() { - use crate::solvers::{BruteForce, Solver}; + use crate::solvers::BruteForce; use crate::traits::Problem; let graph = ReductionGraph::new(); @@ -879,7 +1343,7 @@ fn test_reduction_chain_direct() { let target: &MinimumVertexCover = chain.target_problem(); let solver = BruteForce::new(); - let target_solution = solver.find_best(target).unwrap(); + let target_solution = solver.find_witness(target).unwrap(); let source_solution = chain.extract_solution(&target_solution); let metric = problem.evaluate(&source_solution); assert!(metric.is_valid()); @@ -887,7 +1351,7 @@ fn test_reduction_chain_direct() { #[test] fn test_reduction_chain_multi_step() { - use crate::solvers::{BruteForce, Solver}; + use crate::solvers::BruteForce; use crate::traits::Problem; let graph = ReductionGraph::new(); @@ -914,7 +1378,7 @@ fn test_reduction_chain_multi_step() { let target: &MaximumSetPacking = chain.target_problem(); let solver = BruteForce::new(); - let target_solution = solver.find_best(target).unwrap(); + let target_solution = solver.find_witness(target).unwrap(); let source_solution = chain.extract_solution(&target_solution); let metric = problem.evaluate(&source_solution); assert!(metric.is_valid()); @@ -924,7 +1388,7 @@ fn test_reduction_chain_multi_step() { fn test_reduction_chain_with_variant_casts() { use crate::models::formula::{CNFClause, KSatisfiability}; use crate::rules::MinimizeSteps; - use crate::solvers::{BruteForce, Solver}; + use crate::solvers::BruteForce; use crate::topology::UnitDiskGraph; use crate::traits::Problem; use crate::types::ProblemSize; @@ -965,7 +1429,7 @@ fn test_reduction_chain_with_variant_casts() { let target: &MinimumVertexCover = chain.target_problem(); let solver = BruteForce::new(); - let target_solution = solver.find_best(target).unwrap(); + let target_solution = solver.find_witness(target).unwrap(); let source_solution = chain.extract_solution(&target_solution); let metric = mis.evaluate(&source_solution); assert!(metric.is_valid()); @@ -1007,7 +1471,7 @@ fn test_reduction_chain_with_variant_casts() { .unwrap(); let target: &MaximumIndependentSet = ksat_chain.target_problem(); - let target_solution = solver.find_best(target).unwrap(); + let target_solution = solver.find_witness(target).unwrap(); let original_solution = ksat_chain.extract_solution(&target_solution); // Verify the extracted solution satisfies the original 3-SAT formula diff --git a/src/unit_tests/rules/graphpartitioning_ilp.rs b/src/unit_tests/rules/graphpartitioning_ilp.rs index 30f0ae1eb..bb0ec4e4e 100644 --- a/src/unit_tests/rules/graphpartitioning_ilp.rs +++ b/src/unit_tests/rules/graphpartitioning_ilp.rs @@ -4,7 +4,7 @@ use crate::models::graph::GraphPartitioning; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; fn canonical_instance() -> GraphPartitioning { let graph = SimpleGraph::new( @@ -83,15 +83,15 @@ fn test_graphpartitioning_to_ilp_closed_loop() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_obj = problem.evaluate(&bf_solutions[0]); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution); let ilp_obj = problem.evaluate(&extracted); - assert_eq!(bf_obj, SolutionSize::Valid(3)); - assert_eq!(ilp_obj, SolutionSize::Valid(3)); + assert_eq!(bf_obj, Min(Some(3))); + assert_eq!(ilp_obj, Min(Some(3))); } #[test] @@ -116,7 +116,7 @@ fn test_solution_extraction() { let extracted = reduction.extract_solution(&ilp_solution); assert_eq!(extracted, vec![0, 0, 0, 1, 1, 1]); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(&extracted), Min(Some(3))); } #[test] @@ -128,5 +128,5 @@ fn test_solve_reduced() { .solve_reduced(&problem) .expect("solve_reduced should work"); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(&solution), Min(Some(3))); } diff --git a/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs b/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs index 3bd08f526..de6300cbc 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs @@ -2,9 +2,9 @@ use crate::models::graph::{HamiltonianCircuit, TravelingSalesman}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; use crate::rules::ReduceTo; use crate::rules::ReductionResult; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::{Graph, SimpleGraph}; -use crate::types::SolutionSize; +use crate::types::Min; use crate::Problem; fn cycle4_hc() -> HamiltonianCircuit { @@ -44,13 +44,12 @@ fn test_hamiltoniancircuit_to_travelingsalesman_nonhamiltonian_cost_gap() { let reduction = ReduceTo::>::reduce_to(&source); let target = reduction.target_problem(); let best = BruteForce::new() - .find_best(target) + .find_witness(target) .expect("complete weighted graph should always admit a tour"); - match target.evaluate(&best) { - SolutionSize::Valid(cost) => assert!(cost > 4, "expected cost > 4, got {cost}"), - SolutionSize::Invalid => panic!("best TSP solution evaluated as invalid"), - } + let metric = target.evaluate(&best); + assert!(metric.is_valid(), "best TSP solution evaluated as invalid"); + assert!(metric.unwrap() > 4, "expected cost > 4"); } #[test] @@ -68,7 +67,7 @@ fn test_hamiltoniancircuit_to_travelingsalesman_extract_solution_cycle() { let extracted = reduction.extract_solution(&target_solution); - assert_eq!(target.evaluate(&target_solution), SolutionSize::Valid(4)); + assert_eq!(target.evaluate(&target_solution), Min(Some(4))); assert_eq!(extracted.len(), 4); assert!(source.evaluate(&extracted)); } diff --git a/src/unit_tests/rules/ilp_bool_ilp_i32.rs b/src/unit_tests/rules/ilp_bool_ilp_i32.rs index 4f89d8599..1e824f1fa 100644 --- a/src/unit_tests/rules/ilp_bool_ilp_i32.rs +++ b/src/unit_tests/rules/ilp_bool_ilp_i32.rs @@ -1,6 +1,6 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::rules::traits::{ReduceTo, ReductionResult}; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -19,7 +19,7 @@ fn test_ilp_bool_to_ilp_i32_closed_loop() { // Find optimal on source via brute force let solver = BruteForce::new(); let source_best = solver - .find_best(&source) + .find_witness(&source) .expect("source should have optimal"); let source_obj = source.evaluate(&source_best); diff --git a/src/unit_tests/rules/ilp_qubo.rs b/src/unit_tests/rules/ilp_qubo.rs index eea8f38b8..071d28743 100644 --- a/src/unit_tests/rules/ilp_qubo.rs +++ b/src/unit_tests/rules/ilp_qubo.rs @@ -21,7 +21,7 @@ fn test_ilp_to_qubo_closed_loop() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -49,7 +49,7 @@ fn test_ilp_to_qubo_minimize() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -79,7 +79,7 @@ fn test_ilp_to_qubo_equality() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // Should have exactly 3 optimal solutions (C(3,2)) assert_eq!(qubo_solutions.len(), 3); @@ -113,7 +113,7 @@ fn test_ilp_to_qubo_ge_with_slack() { assert_eq!(qubo.num_variables(), 5); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -147,7 +147,7 @@ fn test_ilp_to_qubo_le_with_slack() { assert_eq!(qubo.num_variables(), 5); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); diff --git a/src/unit_tests/rules/integralflowbundles_ilp.rs b/src/unit_tests/rules/integralflowbundles_ilp.rs index 10455d9c7..944755ef3 100644 --- a/src/unit_tests/rules/integralflowbundles_ilp.rs +++ b/src/unit_tests/rules/integralflowbundles_ilp.rs @@ -1,6 +1,6 @@ use super::*; use crate::models::algebraic::{Comparison, ObjectiveSense, ILP}; -use crate::solvers::{BruteForce, ILPSolver, Solver}; +use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -67,7 +67,7 @@ fn test_integral_flow_bundles_to_ilp_structure() { fn test_integral_flow_bundles_to_ilp_closed_loop() { let problem = yes_instance(); let direct = BruteForce::new() - .find_satisfying(&problem) + .find_witness(&problem) .expect("source instance should be satisfiable"); assert!(problem.evaluate(&direct)); diff --git a/src/unit_tests/rules/knapsack_qubo.rs b/src/unit_tests/rules/knapsack_qubo.rs index 2c18acc1a..568cdab1f 100644 --- a/src/unit_tests/rules/knapsack_qubo.rs +++ b/src/unit_tests/rules/knapsack_qubo.rs @@ -27,7 +27,7 @@ fn test_knapsack_to_qubo_single_item() { assert_eq!(qubo.num_vars(), 2); let solver = BruteForce::new(); - let best_target = solver.find_all_best(qubo); + let best_target = solver.find_all_witnesses(qubo); let extracted = reduction.extract_solution(&best_target[0]); assert_eq!(extracted, vec![1]); } @@ -39,7 +39,7 @@ fn test_knapsack_to_qubo_infeasible_rejected() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let best_target = solver.find_all_best(qubo); + let best_target = solver.find_all_witnesses(qubo); for sol in &best_target { let source_sol = reduction.extract_solution(sol); @@ -60,7 +60,7 @@ fn test_knapsack_to_qubo_empty() { assert_eq!(qubo.num_vars(), 3); let solver = BruteForce::new(); - let best_target = solver.find_all_best(qubo); + let best_target = solver.find_all_witnesses(qubo); let extracted = reduction.extract_solution(&best_target[0]); assert_eq!(extracted, vec![0, 0]); } diff --git a/src/unit_tests/rules/ksatisfiability_qubo.rs b/src/unit_tests/rules/ksatisfiability_qubo.rs index ac7fe8d6f..9b64eccee 100644 --- a/src/unit_tests/rules/ksatisfiability_qubo.rs +++ b/src/unit_tests/rules/ksatisfiability_qubo.rs @@ -21,7 +21,7 @@ fn test_ksatisfiability_to_qubo_closed_loop() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // Verify all solutions satisfy all clauses for sol in &qubo_solutions { @@ -38,7 +38,7 @@ fn test_ksatisfiability_to_qubo_simple() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -62,7 +62,7 @@ fn test_ksatisfiability_to_qubo_contradiction() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // Both x=0 and x=1 satisfy exactly 1 clause assert_eq!(qubo_solutions.len(), 2); @@ -83,7 +83,7 @@ fn test_ksatisfiability_to_qubo_reversed_vars() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -126,7 +126,7 @@ fn test_k3satisfiability_to_qubo_closed_loop() { assert_eq!(qubo.num_variables(), 12); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // Verify all extracted solutions maximize satisfied clauses for sol in &qubo_solutions { @@ -149,7 +149,7 @@ fn test_k3satisfiability_to_qubo_single_clause() { assert_eq!(qubo.num_variables(), 4); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // All solutions should satisfy the single clause for sol in &qubo_solutions { @@ -169,7 +169,7 @@ fn test_k3satisfiability_to_qubo_all_negated() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); diff --git a/src/unit_tests/rules/ksatisfiability_subsetsum.rs b/src/unit_tests/rules/ksatisfiability_subsetsum.rs index fddf38c34..30ffca725 100644 --- a/src/unit_tests/rules/ksatisfiability_subsetsum.rs +++ b/src/unit_tests/rules/ksatisfiability_subsetsum.rs @@ -1,6 +1,6 @@ use super::*; use crate::models::formula::CNFClause; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; use crate::variant::K3; use num_bigint::BigUint; @@ -25,7 +25,7 @@ fn test_ksatisfiability_to_subsetsum_closed_loop() { assert_eq!(target.target(), &BigUint::from(11144u32)); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(target); + let solutions = solver.find_all_witnesses(target); assert!(!solutions.is_empty()); // Every SubsetSum solution must map back to a satisfying 3-SAT assignment @@ -53,7 +53,7 @@ fn test_ksatisfiability_to_subsetsum_unsatisfiable() { let target = reduction.target_problem(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(target); + let solution = solver.find_witness(target); assert!(solution.is_none()); } @@ -68,7 +68,7 @@ fn test_ksatisfiability_to_subsetsum_single_clause() { assert_eq!(target.num_elements(), 8); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(target); + let solutions = solver.find_all_witnesses(target); // Each SubsetSum solution maps to a satisfying assignment let mut sat_assignments = std::collections::HashSet::new(); @@ -118,7 +118,7 @@ fn test_ksatisfiability_to_subsetsum_all_negated() { let target = reduction.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(target); + let solutions = solver.find_all_witnesses(target); let mut sat_assignments = std::collections::HashSet::new(); for sol in &solutions { diff --git a/src/unit_tests/rules/longestcommonsubsequence_ilp.rs b/src/unit_tests/rules/longestcommonsubsequence_ilp.rs index 90cf2aa71..d173fb5f4 100644 --- a/src/unit_tests/rules/longestcommonsubsequence_ilp.rs +++ b/src/unit_tests/rules/longestcommonsubsequence_ilp.rs @@ -1,6 +1,6 @@ use super::*; use crate::models::algebraic::ILP; -use crate::solvers::{BruteForce, ILPSolver, Solver}; +use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; #[test] @@ -48,7 +48,7 @@ fn test_lcs_to_ilp_closed_loop_three_strings() { let brute_force = BruteForce::new(); let witness = brute_force - .find_satisfying(&problem) + .find_witness(&problem) .expect("bruteforce should also find a witness"); assert!(problem.evaluate(&witness)); } diff --git a/src/unit_tests/rules/longestpath_ilp.rs b/src/unit_tests/rules/longestpath_ilp.rs index 6f8411e05..d2aea4305 100644 --- a/src/unit_tests/rules/longestpath_ilp.rs +++ b/src/unit_tests/rules/longestpath_ilp.rs @@ -1,9 +1,9 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; -use crate::solvers::{BruteForce, ILPSolver, Solver}; +use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Max; fn issue_problem() -> LongestPath { LongestPath::new( @@ -59,10 +59,10 @@ fn test_longestpath_to_ilp_closed_loop_on_issue_example() { let problem = issue_problem(); let brute_force = BruteForce::new(); let best = brute_force - .find_best(&problem) + .find_witness(&problem) .expect("brute-force optimum"); let best_value = problem.evaluate(&best); - assert_eq!(best_value, SolutionSize::Valid(20)); + assert_eq!(best_value, Max(Some(20))); let reduction: ReductionLongestPathToILP = ReduceTo::>::reduce_to(&problem); let ilp_solver = ILPSolver::new(); @@ -85,7 +85,7 @@ fn test_solution_extraction_from_handcrafted_ilp_assignment() { let extracted = reduction.extract_solution(&target_solution); assert_eq!(extracted, vec![1, 1]); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(5)); + assert_eq!(problem.evaluate(&extracted), Max(Some(5))); } #[test] @@ -104,5 +104,5 @@ fn test_source_equals_target_uses_empty_path() { let extracted = reduction.extract_solution(&ilp_solution); assert_eq!(extracted, vec![0, 0, 0]); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&extracted), Max(Some(0))); } diff --git a/src/unit_tests/rules/maximumclique_maximumindependentset.rs b/src/unit_tests/rules/maximumclique_maximumindependentset.rs index 8e871f7ba..62520343b 100644 --- a/src/unit_tests/rules/maximumclique_maximumindependentset.rs +++ b/src/unit_tests/rules/maximumclique_maximumindependentset.rs @@ -3,7 +3,6 @@ use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization use crate::solvers::BruteForce; use crate::topology::Graph; use crate::traits::Problem; -use crate::types::SolutionSize; #[test] fn test_maximumclique_to_maximumindependentset_closed_loop() { @@ -44,7 +43,7 @@ fn test_maximumclique_to_maximumindependentset_triangle() { assert_eq!(target.graph().num_edges(), 0); let solver = BruteForce::new(); - let target_solutions = solver.find_all_best(target); + let target_solutions = solver.find_all_witnesses(target); // MIS on empty graph is all vertices selected assert!(target_solutions @@ -53,10 +52,7 @@ fn test_maximumclique_to_maximumindependentset_triangle() { // Extract solution: should be the full clique {0,1,2} let source_sol = reduction.extract_solution(&target_solutions[0]); - assert!(matches!( - source.evaluate(&source_sol), - SolutionSize::Valid(3) - )); + assert_eq!(source.evaluate(&source_sol).unwrap(), 3); } #[test] @@ -80,7 +76,7 @@ fn test_maximumclique_to_maximumindependentset_empty_graph() { assert_eq!(target.graph().num_edges(), 3); let solver = BruteForce::new(); - let target_solutions = solver.find_all_best(target); + let target_solutions = solver.find_all_witnesses(target); // MIS on K3 is any single vertex assert!(target_solutions diff --git a/src/unit_tests/rules/maximumindependentset_gridgraph.rs b/src/unit_tests/rules/maximumindependentset_gridgraph.rs index 52c6ee6ca..d7da5b0af 100644 --- a/src/unit_tests/rules/maximumindependentset_gridgraph.rs +++ b/src/unit_tests/rules/maximumindependentset_gridgraph.rs @@ -44,7 +44,7 @@ fn test_mis_simple_one_to_kings_one_closed_loop() { assert!(target.graph().num_vertices() > 5); let solver = BruteForce::new(); - let grid_solutions = solver.find_all_best(target); + let grid_solutions = solver.find_all_witnesses(target); assert!(!grid_solutions.is_empty()); let original_solution = result.extract_solution(&grid_solutions[0]); diff --git a/src/unit_tests/rules/maximumindependentset_ilp.rs b/src/unit_tests/rules/maximumindependentset_ilp.rs index 122ab6abe..5d2146374 100644 --- a/src/unit_tests/rules/maximumindependentset_ilp.rs +++ b/src/unit_tests/rules/maximumindependentset_ilp.rs @@ -4,7 +4,7 @@ use crate::rules::{MinimizeSteps, ReductionChain, ReductionGraph, ReductionPath} use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::{ProblemSize, SolutionSize}; +use crate::types::{Max, ProblemSize}; fn reduce_mis_to_ilp( problem: &MaximumIndependentSet, @@ -64,7 +64,7 @@ fn test_maximumindependentset_to_ilp_via_path_closed_loop() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = chain.extract_solution(&ilp_solution); @@ -86,6 +86,6 @@ fn test_maximumindependentset_to_ilp_via_path_weighted() { let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = chain.extract_solution(&ilp_solution); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(100)); + assert_eq!(problem.evaluate(&extracted), Max(Some(100))); assert_eq!(extracted, vec![0, 1, 0]); } diff --git a/src/unit_tests/rules/maximumindependentset_maximumclique.rs b/src/unit_tests/rules/maximumindependentset_maximumclique.rs index 9225eaf72..a8d485189 100644 --- a/src/unit_tests/rules/maximumindependentset_maximumclique.rs +++ b/src/unit_tests/rules/maximumindependentset_maximumclique.rs @@ -41,7 +41,7 @@ fn test_maximumindependentset_to_maximumclique_weighted() { // In empty graph, max clique is a single vertex. Best is vertex 2 (weight 30). let solver = BruteForce::new(); - let best = solver.find_all_best(target); + let best = solver.find_all_witnesses(target); for sol in &best { let extracted = reduction.extract_solution(sol); let metric = source.evaluate(&extracted); @@ -62,7 +62,7 @@ fn test_maximumindependentset_to_maximumclique_empty_graph() { // All 4 vertices form a clique in complement = all 4 are independent set in source let solver = BruteForce::new(); - let best_target = solver.find_all_best(target); + let best_target = solver.find_all_witnesses(target); assert!(best_target.iter().all(|s| s.iter().sum::() == 4)); } @@ -80,6 +80,6 @@ fn test_maximumindependentset_to_maximumclique_complete_graph() { // Max clique in empty graph is single vertex, max IS in K4 is also single vertex let solver = BruteForce::new(); - let best = solver.find_all_best(target); + let best = solver.find_all_witnesses(target); assert!(best.iter().all(|s| s.iter().sum::() == 1)); } diff --git a/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs b/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs index 6ed1983c6..880265a7d 100644 --- a/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs +++ b/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs @@ -26,7 +26,7 @@ fn test_empty_graph() { assert_eq!(sp_problem.num_sets(), 3); let solver = BruteForce::new(); - let solutions = solver.find_all_best(sp_problem); + let solutions = solver.find_all_witnesses(sp_problem); // With no overlaps, we can select all sets assert_eq!(solutions[0].iter().sum::(), 3); @@ -81,7 +81,7 @@ fn test_jl_parity_is_to_setpacking() { MaximumIndependentSet::new(SimpleGraph::new(nv, jl_parse_edges(inst)), vec![1i32; nv]); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -104,7 +104,7 @@ fn test_jl_parity_setpacking_to_is() { let source = MaximumSetPacking::::new(jl_parse_sets(&inst["sets"])); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -129,7 +129,7 @@ fn test_jl_parity_rule_is_to_setpacking() { MaximumIndependentSet::new(SimpleGraph::new(nv, jl_parse_edges(inst)), vec![1i32; nv]); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -155,7 +155,7 @@ fn test_jl_parity_doc_is_to_setpacking() { MaximumIndependentSet::new(SimpleGraph::new(nv, jl_parse_edges(inst)), vec![1i32; nv]); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -177,7 +177,7 @@ fn test_maximumindependentset_one_to_maximumsetpacking_closed_loop() { assert_eq!(sp_problem.num_sets(), 3); let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(sp_problem); + let sp_solutions = solver.find_all_witnesses(sp_problem); assert!(!sp_solutions.is_empty()); let original_solution = reduction.extract_solution(&sp_solutions[0]); @@ -197,7 +197,7 @@ fn test_maximumsetpacking_one_to_maximumindependentset_closed_loop() { assert_eq!(is_problem.graph().num_vertices(), 3); let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(is_problem); + let is_solutions = solver.find_all_witnesses(is_problem); assert!(!is_solutions.is_empty()); let original_solution = reduction.extract_solution(&is_solutions[0]); diff --git a/src/unit_tests/rules/maximumindependentset_qubo.rs b/src/unit_tests/rules/maximumindependentset_qubo.rs index 8a7c70365..1e297ba80 100644 --- a/src/unit_tests/rules/maximumindependentset_qubo.rs +++ b/src/unit_tests/rules/maximumindependentset_qubo.rs @@ -1,10 +1,10 @@ use crate::models::algebraic::QUBO; use crate::models::graph::MaximumIndependentSet; use crate::rules::{Minimize, ReductionChain, ReductionGraph, ReductionPath}; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; -use crate::types::{ProblemSize, SolutionSize}; +use crate::types::{Max, ProblemSize}; fn reduce_mis_to_qubo( problem: &MaximumIndependentSet, @@ -51,7 +51,7 @@ fn test_maximumindependentset_to_qubo_via_path_closed_loop() { assert_eq!(qubo.num_variables(), 4); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = chain.extract_solution(sol); assert!(problem.evaluate(&extracted).is_valid()); @@ -68,11 +68,11 @@ fn test_maximumindependentset_to_qubo_via_path_weighted() { let solver = BruteForce::new(); let qubo_solution = solver - .find_best(qubo) + .find_witness(qubo) .expect("QUBO should be solvable via path"); let extracted = chain.extract_solution(&qubo_solution); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(100)); + assert_eq!(problem.evaluate(&extracted), Max(Some(100))); assert_eq!(extracted, vec![0, 1, 0]); } @@ -85,9 +85,9 @@ fn test_maximumindependentset_to_qubo_via_path_empty_graph() { assert_eq!(qubo.num_variables(), 3); let solver = BruteForce::new(); - let qubo_solution = solver.find_best(qubo).expect("QUBO should be solvable"); + let qubo_solution = solver.find_witness(qubo).expect("QUBO should be solvable"); let extracted = chain.extract_solution(&qubo_solution); assert_eq!(extracted, vec![1, 1, 1]); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(&extracted), Max(Some(3))); } diff --git a/src/unit_tests/rules/maximummatching_ilp.rs b/src/unit_tests/rules/maximummatching_ilp.rs index 15b2a5779..dab0c1680 100644 --- a/src/unit_tests/rules/maximummatching_ilp.rs +++ b/src/unit_tests/rules/maximummatching_ilp.rs @@ -2,7 +2,7 @@ use super::*; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Max; #[test] fn test_reduction_creates_valid_ilp() { @@ -55,7 +55,7 @@ fn test_maximummatching_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); // Solve with brute force on original problem - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); // Solve via ILP reduction let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); @@ -64,8 +64,8 @@ fn test_maximummatching_to_ilp_closed_loop() { // Both should find optimal size = 1 (one edge) let bf_size = problem.evaluate(&bf_solutions[0]); let ilp_size = problem.evaluate(&extracted); - assert_eq!(bf_size, SolutionSize::Valid(1)); - assert_eq!(ilp_size, SolutionSize::Valid(1)); + assert_eq!(bf_size, Max(Some(1))); + assert_eq!(ilp_size, Max(Some(1))); // Verify the ILP solution is valid for the original problem assert!( @@ -86,7 +86,7 @@ fn test_ilp_solution_equals_brute_force_path() { let ilp_solver = ILPSolver::new(); // Solve with brute force - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_size = problem.evaluate(&bf_solutions[0]); // Solve via ILP @@ -94,8 +94,8 @@ fn test_ilp_solution_equals_brute_force_path() { let extracted = reduction.extract_solution(&ilp_solution); let ilp_size = problem.evaluate(&extracted); - assert_eq!(bf_size, SolutionSize::Valid(2)); - assert_eq!(ilp_size, SolutionSize::Valid(2)); + assert_eq!(bf_size, Max(Some(2))); + assert_eq!(ilp_size, Max(Some(2))); // Verify validity assert!(problem.evaluate(&extracted).is_valid()); @@ -114,15 +114,15 @@ fn test_ilp_solution_equals_brute_force_weighted() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_obj = problem.evaluate(&bf_solutions[0]); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution); let ilp_obj = problem.evaluate(&extracted); - assert_eq!(bf_obj, SolutionSize::Valid(100)); - assert_eq!(ilp_obj, SolutionSize::Valid(100)); + assert_eq!(bf_obj, Max(Some(100))); + assert_eq!(ilp_obj, Max(Some(100))); // Verify the solution selects edge 0 (0-1) assert_eq!(extracted, vec![1, 0]); @@ -169,7 +169,7 @@ fn test_empty_graph() { assert_eq!(ilp.constraints.len(), 0); assert!(problem.evaluate(&[]).is_valid()); - assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[]), Max(Some(0))); } #[test] @@ -191,7 +191,7 @@ fn test_k4_perfect_matching() { let extracted = reduction.extract_solution(&ilp_solution); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(2)); // Perfect matching has 2 edges + assert_eq!(problem.evaluate(&extracted), Max(Some(2))); // Perfect matching has 2 edges // Verify all vertices are matched let sum: usize = extracted.iter().sum(); @@ -212,7 +212,7 @@ fn test_star_graph() { let extracted = reduction.extract_solution(&ilp_solution); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Max(Some(1))); } #[test] @@ -231,7 +231,7 @@ fn test_bipartite_graph() { let extracted = reduction.extract_solution(&ilp_solution); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&extracted), Max(Some(2))); } #[test] @@ -246,5 +246,5 @@ fn test_solve_reduced() { .expect("solve_reduced should work"); assert!(problem.evaluate(&solution).is_valid()); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&solution), Max(Some(2))); } diff --git a/src/unit_tests/rules/maximummatching_maximumsetpacking.rs b/src/unit_tests/rules/maximummatching_maximumsetpacking.rs index beab0a2c6..b72d0ee3e 100644 --- a/src/unit_tests/rules/maximummatching_maximumsetpacking.rs +++ b/src/unit_tests/rules/maximummatching_maximumsetpacking.rs @@ -3,7 +3,7 @@ use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Max; include!("../jl_helpers.rs"); #[test] @@ -37,21 +37,15 @@ fn test_matching_to_setpacking_weighted() { assert_eq!(sp.weights_ref(), &vec![100, 1, 1]); let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(sp); + let sp_solutions = solver.find_all_witnesses(sp); // Edge 0-1 (weight 100) alone beats edges 0-2 + 1-3 (weight 2) assert!(sp_solutions.contains(&vec![1, 0, 0])); // Verify through direct MaximumMatching solution - let direct_solutions = solver.find_all_best(&matching); - assert_eq!( - matching.evaluate(&sp_solutions[0]), - SolutionSize::Valid(100) - ); - assert_eq!( - matching.evaluate(&direct_solutions[0]), - SolutionSize::Valid(100) - ); + let direct_solutions = solver.find_all_witnesses(&matching); + assert_eq!(matching.evaluate(&sp_solutions[0]), Max(Some(100))); + assert_eq!(matching.evaluate(&direct_solutions[0]), Max(Some(100))); } #[test] @@ -89,7 +83,7 @@ fn test_matching_to_setpacking_single_edge() { assert_eq!(sp.sets()[0], vec![0, 1]); let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(sp); + let sp_solutions = solver.find_all_witnesses(sp); // Should select the only set assert_eq!(sp_solutions, vec![vec![1]]); @@ -104,7 +98,7 @@ fn test_matching_to_setpacking_disjoint_edges() { let sp = reduction.target_problem(); let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(sp); + let sp_solutions = solver.find_all_witnesses(sp); // Both edges can be selected (they don't share vertices) assert_eq!(sp_solutions, vec![vec![1, 1]]); @@ -130,7 +124,7 @@ fn test_matching_to_setpacking_star() { let sp = reduction.target_problem(); let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(sp); + let sp_solutions = solver.find_all_witnesses(sp); // All edges share vertex 0, so max matching = 1 for sol in &sp_solutions { @@ -170,7 +164,8 @@ fn test_jl_parity_matching_to_setpacking() { ); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = + solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, diff --git a/src/unit_tests/rules/maximumsetpacking_casts.rs b/src/unit_tests/rules/maximumsetpacking_casts.rs index 079602986..7932ba4d1 100644 --- a/src/unit_tests/rules/maximumsetpacking_casts.rs +++ b/src/unit_tests/rules/maximumsetpacking_casts.rs @@ -1,7 +1,7 @@ use super::*; use crate::rules::traits::ReductionResult; use crate::rules::ReduceTo; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -14,7 +14,7 @@ fn test_maximumsetpacking_one_to_i32_cast_closed_loop() { assert_eq!(sp_i32.weights_ref(), &vec![1i32, 1, 1]); let solver = BruteForce::new(); - let target_solution = solver.find_best(sp_i32).unwrap(); + let target_solution = solver.find_witness(sp_i32).unwrap(); let source_solution = reduction.extract_solution(&target_solution); let metric = sp_one.evaluate(&source_solution); @@ -31,7 +31,7 @@ fn test_maximumsetpacking_i32_to_f64_cast_closed_loop() { assert_eq!(sp_f64.weights_ref(), &vec![2.0f64, 3.0, 5.0]); let solver = BruteForce::new(); - let target_solution = solver.find_best(sp_f64).unwrap(); + let target_solution = solver.find_witness(sp_f64).unwrap(); let source_solution = reduction.extract_solution(&target_solution); let metric = sp_i32.evaluate(&source_solution); diff --git a/src/unit_tests/rules/maximumsetpacking_ilp.rs b/src/unit_tests/rules/maximumsetpacking_ilp.rs index 8cf4cdc26..ca288083c 100644 --- a/src/unit_tests/rules/maximumsetpacking_ilp.rs +++ b/src/unit_tests/rules/maximumsetpacking_ilp.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Max; #[test] fn test_reduction_creates_valid_ilp() { @@ -47,7 +47,7 @@ fn test_maximumsetpacking_to_ilp_closed_loop() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution); @@ -74,15 +74,15 @@ fn test_ilp_solution_equals_brute_force_weighted() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_obj = problem.evaluate(&bf_solutions[0]); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution); let ilp_obj = problem.evaluate(&extracted); - assert_eq!(bf_obj, SolutionSize::Valid(6)); - assert_eq!(ilp_obj, SolutionSize::Valid(6)); + assert_eq!(bf_obj, Max(Some(6))); + assert_eq!(ilp_obj, Max(Some(6))); assert_eq!(extracted, vec![0, 1, 1]); } @@ -112,7 +112,7 @@ fn test_disjoint_sets() { assert_eq!(extracted, vec![1, 1, 1, 1]); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(4)); + assert_eq!(problem.evaluate(&extracted), Max(Some(4))); } #[test] @@ -125,5 +125,5 @@ fn test_solve_reduced() { .expect("solve_reduced should work"); assert!(problem.evaluate(&solution).is_valid()); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&solution), Max(Some(2))); } diff --git a/src/unit_tests/rules/maximumsetpacking_qubo.rs b/src/unit_tests/rules/maximumsetpacking_qubo.rs index b0274d0fa..dfad90aa2 100644 --- a/src/unit_tests/rules/maximumsetpacking_qubo.rs +++ b/src/unit_tests/rules/maximumsetpacking_qubo.rs @@ -12,7 +12,7 @@ fn test_setpacking_to_qubo_closed_loop() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -29,7 +29,7 @@ fn test_setpacking_to_qubo_disjoint() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -47,7 +47,7 @@ fn test_setpacking_to_qubo_all_overlap() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); diff --git a/src/unit_tests/rules/minimumdominatingset_ilp.rs b/src/unit_tests/rules/minimumdominatingset_ilp.rs index 01cd49e57..073c304ce 100644 --- a/src/unit_tests/rules/minimumdominatingset_ilp.rs +++ b/src/unit_tests/rules/minimumdominatingset_ilp.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; #[test] fn test_reduction_creates_valid_ilp() { @@ -60,7 +60,7 @@ fn test_minimumdominatingset_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); // Solve with brute force on original problem - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_size = problem.evaluate(&bf_solutions[0]); // Solve via ILP reduction @@ -69,8 +69,8 @@ fn test_minimumdominatingset_to_ilp_closed_loop() { let ilp_size = problem.evaluate(&extracted); // Both should find optimal size = 1 (just the center) - assert_eq!(bf_size, SolutionSize::Valid(1)); - assert_eq!(ilp_size, SolutionSize::Valid(1)); + assert_eq!(bf_size, Min(Some(1))); + assert_eq!(ilp_size, Min(Some(1))); // Verify the ILP solution is valid for the original problem assert!( @@ -93,7 +93,7 @@ fn test_ilp_solution_equals_brute_force_path() { let ilp_solver = ILPSolver::new(); // Solve with brute force - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_size = problem.evaluate(&bf_solutions[0]); // Solve via ILP @@ -101,8 +101,8 @@ fn test_ilp_solution_equals_brute_force_path() { let extracted = reduction.extract_solution(&ilp_solution); let ilp_size = problem.evaluate(&extracted); - assert_eq!(bf_size, SolutionSize::Valid(2)); - assert_eq!(ilp_size, SolutionSize::Valid(2)); + assert_eq!(bf_size, Min(Some(2))); + assert_eq!(ilp_size, Min(Some(2))); // Verify validity assert!(problem.evaluate(&extracted).is_valid()); @@ -122,15 +122,15 @@ fn test_ilp_solution_equals_brute_force_weighted() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_obj = problem.evaluate(&bf_solutions[0]); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution); let ilp_obj = problem.evaluate(&extracted); - assert_eq!(bf_obj, SolutionSize::Valid(3)); - assert_eq!(ilp_obj, SolutionSize::Valid(3)); + assert_eq!(bf_obj, Min(Some(3))); + assert_eq!(ilp_obj, Min(Some(3))); // Verify the solution selects all leaves assert_eq!(extracted, vec![0, 1, 1, 1]); @@ -196,7 +196,7 @@ fn test_complete_graph() { let extracted = reduction.extract_solution(&ilp_solution); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); } #[test] @@ -213,7 +213,7 @@ fn test_single_vertex() { assert_eq!(extracted, vec![1]); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); } #[test] @@ -230,7 +230,7 @@ fn test_cycle_graph() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_size = problem.evaluate(&bf_solutions[0]); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); diff --git a/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs b/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs index 66be3cbff..762157e7e 100644 --- a/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs +++ b/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs @@ -2,7 +2,7 @@ use super::*; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; #[test] fn test_reduction_creates_valid_ilp() { @@ -32,7 +32,7 @@ fn test_minimumfeedbackvertexset_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); // Solve with brute force on original problem - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_size = problem.evaluate(&bf_solutions[0]); // Solve via ILP reduction @@ -41,8 +41,8 @@ fn test_minimumfeedbackvertexset_to_ilp_closed_loop() { let ilp_size = problem.evaluate(&extracted); // Both should find optimal size = 1 - assert_eq!(bf_size, SolutionSize::Valid(1)); - assert_eq!(ilp_size, SolutionSize::Valid(1)); + assert_eq!(bf_size, Min(Some(1))); + assert_eq!(ilp_size, Min(Some(1))); // Verify the ILP solution is valid for the original problem assert!( @@ -89,7 +89,7 @@ fn test_cycle_of_triangles() { let extracted = reduction.extract_solution(&ilp_solution); let size = problem.evaluate(&extracted); - assert_eq!(size, SolutionSize::Valid(3), "FVS should be 3"); + assert_eq!(size, Min(Some(3)), "FVS should be 3"); } #[test] @@ -105,7 +105,7 @@ fn test_dag_no_removal() { let extracted = reduction.extract_solution(&ilp_solution); let size = problem.evaluate(&extracted); - assert_eq!(size, SolutionSize::Valid(0), "DAG needs no removal"); + assert_eq!(size, Min(Some(0)), "DAG needs no removal"); assert_eq!(extracted, vec![0, 0, 0]); } @@ -126,7 +126,7 @@ fn test_single_vertex() { let extracted = reduction.extract_solution(&ilp_solution); assert_eq!(extracted, vec![0]); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&extracted), Min(Some(0))); } #[test] @@ -153,7 +153,7 @@ fn test_weighted() { // Should remove vertex 1 (cheapest) assert_eq!(extracted[1], 1, "Should remove vertex 1 (cheapest)"); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); } #[test] @@ -164,7 +164,7 @@ fn test_two_disjoint_cycles() { let problem = MinimumFeedbackVertexSet::new(graph, vec![1i32; 4]); let bf = BruteForce::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_size = problem.evaluate(&bf_solutions[0]); let reduction: ReductionMFVSToILP = ReduceTo::>::reduce_to(&problem); @@ -174,8 +174,8 @@ fn test_two_disjoint_cycles() { let extracted = reduction.extract_solution(&ilp_solution); let ilp_size = problem.evaluate(&extracted); - assert_eq!(bf_size, SolutionSize::Valid(2)); - assert_eq!(ilp_size, SolutionSize::Valid(2)); + assert_eq!(bf_size, Min(Some(2))); + assert_eq!(ilp_size, Min(Some(2))); } #[test] diff --git a/src/unit_tests/rules/minimummultiwaycut_ilp.rs b/src/unit_tests/rules/minimummultiwaycut_ilp.rs index 8dfd32747..4ed31f8b7 100644 --- a/src/unit_tests/rules/minimummultiwaycut_ilp.rs +++ b/src/unit_tests/rules/minimummultiwaycut_ilp.rs @@ -3,7 +3,7 @@ use crate::models::algebraic::ObjectiveSense; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; /// Build the canonical 5-vertex, 3-terminal example from issue #185. fn canonical_instance() -> MinimumMultiwayCut { @@ -37,7 +37,7 @@ fn test_minimummultiwaycut_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); // Solve original with brute force - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_obj = problem.evaluate(&bf_solutions[0]); // Solve via ILP @@ -46,8 +46,8 @@ fn test_minimummultiwaycut_to_ilp_closed_loop() { let ilp_obj = problem.evaluate(&extracted); // Optimal cut cost is 8 - assert_eq!(bf_obj, SolutionSize::Valid(8)); - assert_eq!(ilp_obj, SolutionSize::Valid(8)); + assert_eq!(bf_obj, Min(Some(8))); + assert_eq!(ilp_obj, Min(Some(8))); } #[test] @@ -66,7 +66,7 @@ fn test_triangle_with_3_terminals() { let extracted = reduction.extract_solution(&ilp_solution); let obj = problem.evaluate(&extracted); - assert_eq!(obj, SolutionSize::Valid(6)); + assert_eq!(obj, Min(Some(6))); } #[test] @@ -84,7 +84,7 @@ fn test_two_terminals() { let extracted = reduction.extract_solution(&ilp_solution); let obj = problem.evaluate(&extracted); - assert_eq!(obj, SolutionSize::Valid(1)); + assert_eq!(obj, Min(Some(1))); } #[test] @@ -122,7 +122,7 @@ fn test_solution_extraction() { assert_eq!(extracted, vec![1, 0, 0, 1, 1, 0]); let obj = problem.evaluate(&extracted); - assert_eq!(obj, SolutionSize::Valid(8)); + assert_eq!(obj, Min(Some(8))); } #[test] @@ -135,5 +135,5 @@ fn test_solve_reduced() { .expect("solve_reduced should work"); assert!(problem.evaluate(&solution).is_valid()); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(8)); + assert_eq!(problem.evaluate(&solution), Min(Some(8))); } diff --git a/src/unit_tests/rules/minimummultiwaycut_qubo.rs b/src/unit_tests/rules/minimummultiwaycut_qubo.rs index 5453b9fad..4b42b97c7 100644 --- a/src/unit_tests/rules/minimummultiwaycut_qubo.rs +++ b/src/unit_tests/rules/minimummultiwaycut_qubo.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; #[test] fn test_minimummultiwaycut_to_qubo_closed_loop() { @@ -13,7 +13,7 @@ fn test_minimummultiwaycut_to_qubo_closed_loop() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); assert!(!qubo_solutions.is_empty(), "QUBO solver found no solutions"); @@ -21,7 +21,7 @@ fn test_minimummultiwaycut_to_qubo_closed_loop() { for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); let metric = source.evaluate(&extracted); - assert_eq!(metric, SolutionSize::Valid(8)); + assert_eq!(metric, Min(Some(8))); } } @@ -35,7 +35,7 @@ fn test_minimummultiwaycut_to_qubo_small() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); assert!(!qubo_solutions.is_empty(), "QUBO solver found no solutions"); @@ -44,7 +44,7 @@ fn test_minimummultiwaycut_to_qubo_small() { let extracted = reduction.extract_solution(sol); let metric = source.evaluate(&extracted); // With 2 terminals and path 0-1-2, minimum cut is 1 (cut either edge) - assert_eq!(metric, SolutionSize::Valid(1)); + assert_eq!(metric, Min(Some(1))); } } @@ -70,7 +70,7 @@ fn test_minimummultiwaycut_to_qubo_terminal_pinning() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); let k = terminals.len(); for sol in &qubo_solutions { diff --git a/src/unit_tests/rules/minimumsetcovering_ilp.rs b/src/unit_tests/rules/minimumsetcovering_ilp.rs index 523b6a569..eda045703 100644 --- a/src/unit_tests/rules/minimumsetcovering_ilp.rs +++ b/src/unit_tests/rules/minimumsetcovering_ilp.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; #[test] fn test_reduction_creates_valid_ilp() { @@ -52,7 +52,7 @@ fn test_minimumsetcovering_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); // Solve with brute force on original problem - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); // Solve via ILP reduction let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); @@ -88,15 +88,15 @@ fn test_ilp_solution_equals_brute_force_weighted() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_obj = problem.evaluate(&bf_solutions[0]); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution); let ilp_obj = problem.evaluate(&extracted); - assert_eq!(bf_obj, SolutionSize::Valid(6)); - assert_eq!(ilp_obj, SolutionSize::Valid(6)); + assert_eq!(bf_obj, Min(Some(6))); + assert_eq!(ilp_obj, Min(Some(6))); // Verify the solution selects S1 and S2 assert_eq!(extracted, vec![0, 1, 1]); @@ -143,7 +143,7 @@ fn test_single_set_covers_all() { assert_eq!(extracted, vec![1, 0, 0, 0]); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); } #[test] @@ -162,7 +162,7 @@ fn test_overlapping_sets() { assert_eq!(extracted, vec![1, 1]); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&extracted), Min(Some(2))); } #[test] @@ -188,7 +188,7 @@ fn test_solve_reduced() { .expect("solve_reduced should work"); assert!(problem.evaluate(&solution).is_valid()); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&solution), Min(Some(2))); } #[test] diff --git a/src/unit_tests/rules/minimumvertexcover_ilp.rs b/src/unit_tests/rules/minimumvertexcover_ilp.rs index d58d372f5..57f47c507 100644 --- a/src/unit_tests/rules/minimumvertexcover_ilp.rs +++ b/src/unit_tests/rules/minimumvertexcover_ilp.rs @@ -4,7 +4,7 @@ use crate::rules::{MinimizeSteps, ReductionChain, ReductionGraph, ReductionPath} use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::{ProblemSize, SolutionSize}; +use crate::types::{Min, ProblemSize}; fn reduce_vc_to_ilp( problem: &MinimumVertexCover, @@ -61,7 +61,7 @@ fn test_minimumvertexcover_to_ilp_via_path_closed_loop() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = chain.extract_solution(&ilp_solution); @@ -83,6 +83,6 @@ fn test_minimumvertexcover_to_ilp_via_path_weighted() { let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = chain.extract_solution(&ilp_solution); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); assert_eq!(extracted, vec![0, 1, 0]); } diff --git a/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs b/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs index dd4533fd6..850680ec7 100644 --- a/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs +++ b/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs @@ -42,7 +42,7 @@ fn test_jl_parity_is_to_vertexcovering() { MaximumIndependentSet::new(SimpleGraph::new(nv, jl_parse_edges(inst)), vec![1i32; nv]); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target(&source, &result, "JL parity MIS->VC"); for case in data["cases"].as_array().unwrap() { assert_eq!(best_source, jl_parse_configs_set(&case["best_source"])); @@ -63,7 +63,7 @@ fn test_jl_parity_rule_is_to_vertexcovering() { MaximumIndependentSet::new(SimpleGraph::new(nv, jl_parse_edges(inst)), vec![1i32; nv]); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, diff --git a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs index 28816eea5..4ae5fd263 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs @@ -6,7 +6,7 @@ use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization use crate::rules::traits::ReductionResult; use crate::rules::ReduceTo; #[cfg(feature = "example-db")] -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::{Graph, SimpleGraph}; #[cfg(feature = "example-db")] use crate::traits::Problem; @@ -119,10 +119,10 @@ fn test_canonical_rule_example_spec_builds() { ); let best_source = BruteForce::new() - .find_best(&source) + .find_witness(&source) .expect("source example should have an optimum"); let best_target = BruteForce::new() - .find_best(&target) + .find_witness(&target) .expect("target example should have an optimum"); assert_eq!(source_metric, source.evaluate(&best_source)); diff --git a/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs b/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs index d3387ac46..1214555e9 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs @@ -60,8 +60,8 @@ fn test_vc_to_sc_weighted() { // Solve both ways let solver = BruteForce::new(); - let vc_solutions = solver.find_all_best(&vc_problem); - let sc_solutions = solver.find_all_best(sc_problem); + let vc_solutions = solver.find_all_witnesses(&vc_problem); + let sc_solutions = solver.find_all_witnesses(sc_problem); // Both should select vertex 1 (weight 1) assert_eq!(vc_solutions[0], vec![0, 1, 0]); @@ -104,7 +104,7 @@ fn test_vc_to_sc_star_graph() { // Minimum cover should be just vertex 0 let solver = BruteForce::new(); - let solutions = solver.find_all_best(&vc_problem); + let solutions = solver.find_all_witnesses(&vc_problem); assert_eq!(solutions[0], vec![1, 0, 0, 0]); } @@ -124,7 +124,7 @@ fn test_jl_parity_vc_to_setcovering() { ); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -151,7 +151,7 @@ fn test_jl_parity_rule_vc_to_setcovering() { ); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, diff --git a/src/unit_tests/rules/minimumvertexcover_qubo.rs b/src/unit_tests/rules/minimumvertexcover_qubo.rs index 95bdff9d6..8b4dd1711 100644 --- a/src/unit_tests/rules/minimumvertexcover_qubo.rs +++ b/src/unit_tests/rules/minimumvertexcover_qubo.rs @@ -1,10 +1,10 @@ use crate::models::algebraic::QUBO; use crate::models::graph::MinimumVertexCover; use crate::rules::{Minimize, ReductionChain, ReductionGraph, ReductionPath}; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; -use crate::types::{ProblemSize, SolutionSize}; +use crate::types::{Min, ProblemSize}; fn reduce_vc_to_qubo( problem: &MinimumVertexCover, @@ -56,7 +56,7 @@ fn test_minimumvertexcover_to_qubo_via_path_closed_loop() { assert_eq!(qubo.num_variables(), 4); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = chain.extract_solution(sol); assert!(problem.evaluate(&extracted).is_valid()); @@ -73,11 +73,11 @@ fn test_minimumvertexcover_to_qubo_via_path_weighted() { let solver = BruteForce::new(); let qubo_solution = solver - .find_best(qubo) + .find_witness(qubo) .expect("QUBO should be solvable via path"); let extracted = chain.extract_solution(&qubo_solution); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); assert_eq!(extracted, vec![0, 1, 0]); } @@ -93,9 +93,9 @@ fn test_minimumvertexcover_to_qubo_via_path_star_graph() { assert_eq!(qubo.num_variables(), 4); let solver = BruteForce::new(); - let qubo_solution = solver.find_best(qubo).expect("QUBO should be solvable"); + let qubo_solution = solver.find_witness(qubo).expect("QUBO should be solvable"); let extracted = chain.extract_solution(&qubo_solution); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); assert_eq!(extracted.iter().filter(|&&x| x == 1).count(), 1); } diff --git a/src/unit_tests/rules/partition_knapsack.rs b/src/unit_tests/rules/partition_knapsack.rs index 127247788..e308d172c 100644 --- a/src/unit_tests/rules/partition_knapsack.rs +++ b/src/unit_tests/rules/partition_knapsack.rs @@ -1,9 +1,9 @@ use super::*; use crate::models::misc::Partition; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Max; #[test] fn test_partition_to_knapsack_closed_loop() { @@ -35,10 +35,10 @@ fn test_partition_to_knapsack_odd_total_is_not_satisfying() { let reduction = ReduceTo::::reduce_to(&source); let target = reduction.target_problem(); let best = BruteForce::new() - .find_best(target) + .find_witness(target) .expect("Knapsack target should always have an optimal solution"); - assert_eq!(target.evaluate(&best), SolutionSize::Valid(5)); + assert_eq!(target.evaluate(&best), Max(Some(5))); let extracted = reduction.extract_solution(&best); assert!(!source.evaluate(&extracted)); diff --git a/src/unit_tests/rules/qubo_ilp.rs b/src/unit_tests/rules/qubo_ilp.rs index e95c8f568..924a25723 100644 --- a/src/unit_tests/rules/qubo_ilp.rs +++ b/src/unit_tests/rules/qubo_ilp.rs @@ -30,7 +30,7 @@ fn test_qubo_to_ilp_diagonal_only() { assert!(ilp.constraints.is_empty()); let solver = BruteForce::new(); - let best = solver.find_all_best(ilp); + let best = solver.find_all_witnesses(ilp); let extracted = reduction.extract_solution(&best[0]); assert_eq!(extracted, vec![0, 1]); } @@ -53,7 +53,7 @@ fn test_qubo_to_ilp_3var() { assert_eq!(ilp.constraints.len(), 6); let solver = BruteForce::new(); - let best = solver.find_all_best(ilp); + let best = solver.find_all_witnesses(ilp); let extracted = reduction.extract_solution(&best[0]); assert_eq!(extracted, vec![1, 0, 1]); } diff --git a/src/unit_tests/rules/reduction_path_parity.rs b/src/unit_tests/rules/reduction_path_parity.rs index 9388fa5c9..f86c1ec1d 100644 --- a/src/unit_tests/rules/reduction_path_parity.rs +++ b/src/unit_tests/rules/reduction_path_parity.rs @@ -7,7 +7,7 @@ use crate::models::graph::{MaxCut, SpinGlass}; use crate::models::misc::Factoring; use crate::rules::test_helpers::assert_optimization_round_trip_chain; use crate::rules::{MinimizeSteps, ReductionGraph}; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::ProblemSize; @@ -58,7 +58,7 @@ fn test_jl_parity_maxcut_to_spinglass_path() { assert_eq!(SpinGlass::::NAME, "SpinGlass"); let solver = BruteForce::new(); - let target_solution = solver.find_best(target).unwrap(); + let target_solution = solver.find_witness(target).unwrap(); let source_solution = chain.extract_solution(&target_solution); // Source solution should be valid @@ -115,7 +115,7 @@ fn test_jl_parity_maxcut_to_qubo_path() { /// Julia: factoring = Factoring(2, 1, 3) /// Julia: paths = reduction_paths(Factoring, SpinGlass) -/// Julia: all(solution_size.(Ref(factoring), extract_solution.(Ref(res), sol)) .== Ref(SolutionSize(0, true))) +/// Julia: all(solution_size.(Ref(factoring), extract_solution.(Ref(res), sol)) .== Ref(valid objective 0)) #[cfg(feature = "ilp-solver")] #[test] fn test_jl_parity_factoring_to_spinglass_path() { diff --git a/src/unit_tests/rules/registry.rs b/src/unit_tests/rules/registry.rs index c6b0f4cbd..21ba8dde5 100644 --- a/src/unit_tests/rules/registry.rs +++ b/src/unit_tests/rules/registry.rs @@ -1,5 +1,6 @@ use super::*; use crate::expr::Expr; +use crate::rules::registry::EdgeCapabilities; use std::path::Path; /// Dummy reduce_fn for unit tests that don't exercise runtime reduction. @@ -7,6 +8,12 @@ fn dummy_reduce_fn(_: &dyn std::any::Any) -> Box Box { + unimplemented!("dummy reduce_aggregate_fn for testing") +} + fn dummy_overhead_eval_fn(_: &dyn std::any::Any) -> ProblemSize { ProblemSize::new(vec![]) } @@ -40,7 +47,9 @@ fn test_reduction_entry_overhead() { target_variant_fn: || vec![("graph", "SimpleGraph"), ("weight", "One")], overhead_fn: || ReductionOverhead::new(vec![("n", Expr::Const(2.0) * Expr::Var("n"))]), module_path: "test::module", - reduce_fn: dummy_reduce_fn, + reduce_fn: Some(dummy_reduce_fn), + reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), overhead_eval_fn: dummy_overhead_eval_fn, }; @@ -59,7 +68,9 @@ fn test_reduction_entry_debug() { target_variant_fn: || vec![("graph", "SimpleGraph"), ("weight", "One")], overhead_fn: || ReductionOverhead::default(), module_path: "test::module", - reduce_fn: dummy_reduce_fn, + reduce_fn: Some(dummy_reduce_fn), + reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), overhead_eval_fn: dummy_overhead_eval_fn, }; @@ -77,7 +88,9 @@ fn test_is_base_reduction_unweighted() { target_variant_fn: || vec![("graph", "SimpleGraph"), ("weight", "One")], overhead_fn: || ReductionOverhead::default(), module_path: "test::module", - reduce_fn: dummy_reduce_fn, + reduce_fn: Some(dummy_reduce_fn), + reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), overhead_eval_fn: dummy_overhead_eval_fn, }; assert!(entry.is_base_reduction()); @@ -92,7 +105,9 @@ fn test_is_base_reduction_source_weighted() { target_variant_fn: || vec![("graph", "SimpleGraph"), ("weight", "One")], overhead_fn: || ReductionOverhead::default(), module_path: "test::module", - reduce_fn: dummy_reduce_fn, + reduce_fn: Some(dummy_reduce_fn), + reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), overhead_eval_fn: dummy_overhead_eval_fn, }; assert!(!entry.is_base_reduction()); @@ -107,7 +122,9 @@ fn test_is_base_reduction_target_weighted() { target_variant_fn: || vec![("graph", "SimpleGraph"), ("weight", "f64")], overhead_fn: || ReductionOverhead::default(), module_path: "test::module", - reduce_fn: dummy_reduce_fn, + reduce_fn: Some(dummy_reduce_fn), + reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), overhead_eval_fn: dummy_overhead_eval_fn, }; assert!(!entry.is_base_reduction()); @@ -122,7 +139,9 @@ fn test_is_base_reduction_both_weighted() { target_variant_fn: || vec![("graph", "SimpleGraph"), ("weight", "f64")], overhead_fn: || ReductionOverhead::default(), module_path: "test::module", - reduce_fn: dummy_reduce_fn, + reduce_fn: Some(dummy_reduce_fn), + reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), overhead_eval_fn: dummy_overhead_eval_fn, }; assert!(!entry.is_base_reduction()); @@ -138,12 +157,33 @@ fn test_is_base_reduction_no_weight_key() { target_variant_fn: || vec![("graph", "SimpleGraph")], overhead_fn: || ReductionOverhead::default(), module_path: "test::module", - reduce_fn: dummy_reduce_fn, + reduce_fn: Some(dummy_reduce_fn), + reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), overhead_eval_fn: dummy_overhead_eval_fn, }; assert!(entry.is_base_reduction()); } +#[test] +fn test_reduction_entry_can_store_aggregate_executor() { + let entry = ReductionEntry { + source_name: "A", + target_name: "B", + source_variant_fn: || vec![("graph", "SimpleGraph")], + target_variant_fn: || vec![("graph", "SimpleGraph")], + overhead_fn: || ReductionOverhead::default(), + module_path: "test::module", + reduce_fn: None, + reduce_aggregate_fn: Some(dummy_reduce_aggregate_fn), + capabilities: EdgeCapabilities::aggregate_only(), + overhead_eval_fn: dummy_overhead_eval_fn, + }; + + assert!(entry.reduce_fn.is_none()); + assert!(entry.reduce_aggregate_fn.is_some()); +} + #[test] fn test_reduction_entries_registered() { let entries: Vec<_> = inventory::iter::().collect(); @@ -406,3 +446,32 @@ fn repo_reductions_use_overhead_only_attribute() { offenders, ); } + +#[test] +fn test_edge_capabilities_constructors() { + let wo = EdgeCapabilities::witness_only(); + assert!(wo.witness); + assert!(!wo.aggregate); + + let ao = EdgeCapabilities::aggregate_only(); + assert!(!ao.witness); + assert!(ao.aggregate); + + let both = EdgeCapabilities::both(); + assert!(both.witness); + assert!(both.aggregate); +} + +#[test] +fn test_edge_capabilities_default_is_witness_only() { + let default = EdgeCapabilities::default(); + assert_eq!(default, EdgeCapabilities::witness_only()); +} + +#[test] +fn test_edge_capabilities_serde_roundtrip() { + let caps = EdgeCapabilities::both(); + let json = serde_json::to_string(&caps).unwrap(); + let back: EdgeCapabilities = serde_json::from_str(&json).unwrap(); + assert_eq!(caps, back); +} diff --git a/src/unit_tests/rules/sat_circuitsat.rs b/src/unit_tests/rules/sat_circuitsat.rs index 85a403ea9..6fb021b05 100644 --- a/src/unit_tests/rules/sat_circuitsat.rs +++ b/src/unit_tests/rules/sat_circuitsat.rs @@ -30,7 +30,7 @@ fn test_sat_to_circuitsat_unsatisfiable() { let sat = Satisfiability::new(1, vec![CNFClause::new(vec![1]), CNFClause::new(vec![-1])]); let result = ReduceTo::::reduce_to(&sat); let solver = BruteForce::new(); - let best_target = solver.find_all_satisfying(result.target_problem()); + let best_target = solver.find_all_witnesses(result.target_problem()); assert!( best_target.is_empty(), "Unsatisfiable SAT -> CircuitSAT should have no solutions" diff --git a/src/unit_tests/rules/sat_coloring.rs b/src/unit_tests/rules/sat_coloring.rs index 0c058e24a..929bb7651 100644 --- a/src/unit_tests/rules/sat_coloring.rs +++ b/src/unit_tests/rules/sat_coloring.rs @@ -73,9 +73,9 @@ fn test_unsatisfiable_formula() { let reduction = ReduceTo::>::reduce_to(&sat); let coloring = reduction.target_problem(); - // Solve the coloring problem - use find_all_satisfying since KColoring is a satisfaction problem + // Solve the coloring problem - use find_all_witnesses since KColoring is a satisfaction problem let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(coloring); + let solutions = solver.find_all_witnesses(coloring); // For an unsatisfiable formula, the coloring should have no valid solutions // OR no valid coloring exists that extracts to a satisfying SAT assignment @@ -190,7 +190,7 @@ fn test_single_literal_clauses() { let coloring = reduction.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(coloring); + let solutions = solver.find_all_witnesses(coloring); let mut found_correct = false; for sol in &solutions { @@ -325,7 +325,7 @@ fn test_jl_parity_sat_to_coloring() { .expect("ILP should find a coloring"); let extracted = result.extract_solution(&target_sol); let best_source: HashSet> = BruteForce::new() - .find_all_satisfying(&source) + .find_all_witnesses(&source) .into_iter() .collect(); assert!( diff --git a/src/unit_tests/rules/sat_ksat.rs b/src/unit_tests/rules/sat_ksat.rs index 0748038f3..3c20a3c05 100644 --- a/src/unit_tests/rules/sat_ksat.rs +++ b/src/unit_tests/rules/sat_ksat.rs @@ -117,11 +117,11 @@ fn test_sat_to_ksat_closed_loop() { let reduction = ReduceTo::>::reduce_to(&sat); let ksat = reduction.target_problem(); - // Solve both problems - use find_all_satisfying for satisfaction problems + // Solve both problems - use find_all_witnesses for satisfaction problems let solver = BruteForce::new(); - let sat_solutions = solver.find_all_satisfying(&sat); - let ksat_solutions = solver.find_all_satisfying(ksat); + let sat_solutions = solver.find_all_witnesses(&sat); + let ksat_solutions = solver.find_all_witnesses(ksat); // If SAT is satisfiable, K-SAT should be too let sat_satisfiable = !sat_solutions.is_empty(); @@ -146,9 +146,9 @@ fn test_sat_to_3sat_solution_extraction() { let reduction = ReduceTo::>::reduce_to(&sat); let ksat = reduction.target_problem(); - // Solve K-SAT - use find_all_satisfying for satisfaction problems + // Solve K-SAT - use find_all_witnesses for satisfaction problems let solver = BruteForce::new(); - let ksat_solutions = solver.find_all_satisfying(ksat); + let ksat_solutions = solver.find_all_witnesses(ksat); // Extract and verify solutions for ksat_sol in &ksat_solutions { @@ -208,12 +208,12 @@ fn test_roundtrip_sat_3sat_sat() { let to_sat = ReduceTo::::reduce_to(ksat); let final_sat = to_sat.target_problem(); - // Solve all three - use find_all_satisfying for satisfaction problems + // Solve all three - use find_all_witnesses for satisfaction problems let solver = BruteForce::new(); - let orig_solutions = solver.find_all_satisfying(&original_sat); - let ksat_solutions = solver.find_all_satisfying(ksat); - let final_solutions = solver.find_all_satisfying(final_sat); + let orig_solutions = solver.find_all_witnesses(&original_sat); + let ksat_solutions = solver.find_all_witnesses(ksat); + let final_solutions = solver.find_all_witnesses(final_sat); // All should be satisfiable (have at least one solution) assert!(!orig_solutions.is_empty()); @@ -286,7 +286,7 @@ fn test_mixed_clause_sizes() { assert_eq!(clause.len(), 3); } - // Verify satisfiability is preserved - use find_all_satisfying for satisfaction problems + // Verify satisfiability is preserved - use find_all_witnesses for satisfaction problems assert_satisfaction_round_trip_from_satisfaction_target( &sat, &reduction, @@ -303,8 +303,8 @@ fn test_unsatisfiable_formula() { let ksat = reduction.target_problem(); let solver = BruteForce::new(); - let best_target = solver.find_all_satisfying(ksat); - let best_source: HashSet> = solver.find_all_satisfying(&sat).into_iter().collect(); + let best_target = solver.find_all_witnesses(ksat); + let best_source: HashSet> = solver.find_all_witnesses(&sat).into_iter().collect(); // Both should be empty (unsatisfiable) assert!(best_source.is_empty()); @@ -324,8 +324,7 @@ fn test_jl_parity_sat_to_ksat() { let source = Satisfiability::new(num_vars, clauses); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = - solver.find_all_satisfying(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_satisfaction_round_trip_from_satisfaction_target( &source, &result, @@ -349,8 +348,7 @@ fn test_jl_parity_ksat_to_sat() { let source = KSatisfiability::::new(num_vars, clauses); let result = ReduceTo::::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = - solver.find_all_satisfying(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_satisfaction_round_trip_from_satisfaction_target( &source, &result, @@ -374,8 +372,7 @@ fn test_jl_parity_rule_sat_to_ksat() { let source = Satisfiability::new(num_vars, clauses); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = - solver.find_all_satisfying(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_satisfaction_round_trip_from_satisfaction_target( &source, &result, diff --git a/src/unit_tests/rules/sat_maximumindependentset.rs b/src/unit_tests/rules/sat_maximumindependentset.rs index bc4ba6af8..9c2bd8f12 100644 --- a/src/unit_tests/rules/sat_maximumindependentset.rs +++ b/src/unit_tests/rules/sat_maximumindependentset.rs @@ -72,7 +72,7 @@ fn test_two_clause_sat_to_is() { // Maximum IS should have size 1 (can't select both) let solver = BruteForce::new(); - let solutions = solver.find_all_best(is_problem); + let solutions = solver.find_all_witnesses(is_problem); for sol in &solutions { assert_eq!(sol.iter().sum::(), 1); } @@ -212,7 +212,7 @@ fn test_jl_parity_sat_to_independentset() { let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); let sat_solutions: HashSet> = - solver.find_all_satisfying(&source).into_iter().collect(); + solver.find_all_witnesses(&source).into_iter().collect(); for case in data["cases"].as_array().unwrap() { if sat_solutions.is_empty() { let target_solution = solve_optimization_problem(result.target_problem()) diff --git a/src/unit_tests/rules/sat_minimumdominatingset.rs b/src/unit_tests/rules/sat_minimumdominatingset.rs index 50902fda3..a421375b0 100644 --- a/src/unit_tests/rules/sat_minimumdominatingset.rs +++ b/src/unit_tests/rules/sat_minimumdominatingset.rs @@ -200,7 +200,7 @@ fn test_jl_parity_sat_to_dominatingset() { let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); let sat_solutions: HashSet> = - solver.find_all_satisfying(&source).into_iter().collect(); + solver.find_all_witnesses(&source).into_iter().collect(); for case in data["cases"].as_array().unwrap() { if sat_solutions.is_empty() { let target_solution = solve_optimization_problem(result.target_problem()) diff --git a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index f62686b51..3f39efa50 100644 --- a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -1,9 +1,9 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; -use crate::solvers::{BruteForce, ILPSolver, Solver}; +use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; #[test] fn test_reduction_creates_expected_ilp_shape() { @@ -44,7 +44,7 @@ fn test_extract_solution_encodes_schedule_as_lehmer_code() { // y_{0,1} = 0 means task 1 before task 0. let extracted = reduction.extract_solution(&[3, 1, 0]); assert_eq!(extracted, vec![1, 0]); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(14)); + assert_eq!(problem.evaluate(&extracted), Min(Some(14))); } #[test] @@ -61,7 +61,7 @@ fn test_issue_example_closed_loop() { let extracted = reduction.extract_solution(&ilp_solution); assert_eq!(extracted, vec![1, 2, 0, 1, 0]); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(46)); + assert_eq!(problem.evaluate(&extracted), Min(Some(46))); } #[test] @@ -74,7 +74,7 @@ fn test_ilp_matches_bruteforce_optimum() { let brute_force = BruteForce::new(); let brute_force_solution = brute_force - .find_best(&problem) + .find_witness(&problem) .expect("brute force should find a schedule"); let brute_force_metric = problem.evaluate(&brute_force_solution); @@ -155,5 +155,5 @@ fn test_solve_reduced_matches_source_optimum() { let source_solution = reduction.extract_solution(&ilp_solution); assert_eq!(source_solution, vec![1, 2, 0, 1, 0]); - assert_eq!(problem.evaluate(&source_solution), SolutionSize::Valid(46)); + assert_eq!(problem.evaluate(&source_solution), Min(Some(46))); } diff --git a/src/unit_tests/rules/spinglass_maxcut.rs b/src/unit_tests/rules/spinglass_maxcut.rs index 013bc4777..b6dcac86d 100644 --- a/src/unit_tests/rules/spinglass_maxcut.rs +++ b/src/unit_tests/rules/spinglass_maxcut.rs @@ -97,7 +97,7 @@ fn test_jl_parity_spinglass_to_maxcut() { let source = SpinGlass::::new(nv, interactions, h_values); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -124,7 +124,7 @@ fn test_jl_parity_maxcut_to_spinglass() { let source = MaxCut::new(SimpleGraph::new(nv, edges), weights); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -153,7 +153,7 @@ fn test_jl_parity_rule_maxcut_to_spinglass() { ); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -181,7 +181,7 @@ fn test_jl_parity_rule_spinglass_to_maxcut() { let source = SpinGlass::::new(nv, interactions, h_values); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, diff --git a/src/unit_tests/rules/spinglass_qubo.rs b/src/unit_tests/rules/spinglass_qubo.rs index 633cb95b5..dc3494c02 100644 --- a/src/unit_tests/rules/spinglass_qubo.rs +++ b/src/unit_tests/rules/spinglass_qubo.rs @@ -12,7 +12,7 @@ fn test_spinglass_to_qubo_closed_loop() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); // Anti-ferromagnetic: opposite spins are optimal for sol in &solutions { @@ -33,7 +33,7 @@ fn test_with_onsite_fields() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![0], "Should prefer x=0 (s=-1)"); @@ -84,7 +84,7 @@ fn test_jl_parity_spinglass_to_qubo() { let source = SpinGlass::::new(nv, interactions, h_values); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -126,7 +126,7 @@ fn test_jl_parity_qubo_to_spinglass() { let source = QUBO::from_matrix(rust_matrix); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -169,7 +169,7 @@ fn test_jl_parity_rule_qubo_to_spinglass() { let source = QUBO::from_matrix(rust_matrix); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, diff --git a/src/unit_tests/rules/steinertree_ilp.rs b/src/unit_tests/rules/steinertree_ilp.rs index 25420eaa9..24a7436a0 100644 --- a/src/unit_tests/rules/steinertree_ilp.rs +++ b/src/unit_tests/rules/steinertree_ilp.rs @@ -5,7 +5,7 @@ use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; fn canonical_instance() -> SteinerTree { let graph = SimpleGraph::new( @@ -46,12 +46,12 @@ fn test_steinertree_to_ilp_closed_loop() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let best_source = bf.find_all_best(&problem); + let best_source = bf.find_all_witnesses(&problem); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution); - assert_eq!(problem.evaluate(&best_source[0]), SolutionSize::Valid(6)); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(6)); + assert_eq!(problem.evaluate(&best_source[0]), Min(Some(6))); + assert_eq!(problem.evaluate(&extracted), Min(Some(6))); assert!(problem.is_valid_solution(&extracted)); } @@ -77,7 +77,7 @@ fn test_solve_reduced_uses_new_rule() { let solution = ILPSolver::new() .solve_reduced(&problem) .expect("solve_reduced should find the Steiner tree via ILP"); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(6)); + assert_eq!(problem.evaluate(&solution), Min(Some(6))); } #[test] diff --git a/src/unit_tests/rules/subsetsum_closestvectorproblem.rs b/src/unit_tests/rules/subsetsum_closestvectorproblem.rs index 8eb81def0..818038c3a 100644 --- a/src/unit_tests/rules/subsetsum_closestvectorproblem.rs +++ b/src/unit_tests/rules/subsetsum_closestvectorproblem.rs @@ -1,9 +1,9 @@ use super::*; use crate::models::algebraic::{ClosestVectorProblem, VarBounds}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; use std::collections::HashSet; #[test] @@ -42,7 +42,7 @@ fn test_subsetsum_to_closestvectorproblem_issue_example_minimizers() { let reduction = ReduceTo::>::reduce_to(&source); let target = reduction.target_problem(); let solutions: HashSet> = BruteForce::new() - .find_all_best(target) + .find_all_witnesses(target) .into_iter() .collect(); @@ -50,7 +50,7 @@ fn test_subsetsum_to_closestvectorproblem_issue_example_minimizers() { assert_eq!(solutions, expected); for solution in &solutions { - assert_eq!(target.evaluate(solution), SolutionSize::Valid(1.0)); + assert_eq!(target.evaluate(solution), Min(Some(1.0))); } } @@ -60,13 +60,12 @@ fn test_subsetsum_to_closestvectorproblem_unsatisfiable_instance() { let reduction = ReduceTo::>::reduce_to(&source); let target = reduction.target_problem(); let best = BruteForce::new() - .find_best(target) + .find_witness(target) .expect("unsatisfiable instance should still have a best CVP assignment"); - match target.evaluate(&best) { - SolutionSize::Valid(value) => assert!(value > (source.num_elements() as f64).sqrt() / 2.0), - SolutionSize::Invalid => panic!("CVP solution should be valid"), - } + let metric = target.evaluate(&best); + assert!(metric.is_valid(), "CVP solution should be valid"); + assert!(metric.unwrap() > (source.num_elements() as f64).sqrt() / 2.0); } #[test] diff --git a/src/unit_tests/rules/traits.rs b/src/unit_tests/rules/traits.rs index ab3bff0c8..a7c1acd69 100644 --- a/src/unit_tests/rules/traits.rs +++ b/src/unit_tests/rules/traits.rs @@ -3,8 +3,13 @@ fn test_traits_compile() { // Traits should compile - actual tests in reduction implementations } -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{ + AggregateReductionResult, DynAggregateReductionResult, ReduceTo, ReduceToAggregate, + ReductionResult, +}; use crate::traits::Problem; +use crate::types::Sum; +use serde_json::json; #[derive(Clone)] struct SourceProblem; @@ -13,7 +18,7 @@ struct TargetProblem; impl Problem for SourceProblem { const NAME: &'static str = "Source"; - type Metric = i32; + type Value = i32; fn dims(&self) -> Vec { vec![2, 2] } @@ -27,7 +32,7 @@ impl Problem for SourceProblem { impl Problem for TargetProblem { const NAME: &'static str = "Target"; - type Metric = i32; + type Value = i32; fn dims(&self) -> Vec { vec![2, 2] } @@ -72,3 +77,98 @@ fn test_reduction() { assert_eq!(target.evaluate(&[1, 1]), 2); assert_eq!(result.extract_solution(&[1, 0]), vec![1, 0]); } + +#[derive(Clone)] +struct AggregateSourceProblem; + +#[derive(Clone)] +struct AggregateTargetProblem; + +impl Problem for AggregateSourceProblem { + const NAME: &'static str = "AggregateSource"; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![2] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config.iter().sum::() as u64) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +impl Problem for AggregateTargetProblem { + const NAME: &'static str = "AggregateTarget"; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![2] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config.iter().sum::() as u64) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +struct TestAggregateReduction { + target: AggregateTargetProblem, + offset: u64, +} + +impl AggregateReductionResult for TestAggregateReduction { + type Source = AggregateSourceProblem; + type Target = AggregateTargetProblem; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, target_value: Sum) -> Sum { + Sum(target_value.0 + self.offset) + } +} + +impl ReduceToAggregate for AggregateSourceProblem { + type Result = TestAggregateReduction; + + fn reduce_to_aggregate(&self) -> Self::Result { + TestAggregateReduction { + target: AggregateTargetProblem, + offset: 3, + } + } +} + +#[test] +fn test_aggregate_reduction_extracts_value() { + let source = AggregateSourceProblem; + let result = + >::reduce_to_aggregate( + &source, + ); + + assert_eq!(result.extract_value(Sum(7)), Sum(10)); +} + +#[test] +fn test_dyn_aggregate_reduction_result_extracts_value() { + let result = TestAggregateReduction { + target: AggregateTargetProblem, + offset: 2, + }; + let dyn_result: &dyn DynAggregateReductionResult = &result; + + assert!(dyn_result + .target_problem_any() + .downcast_ref::() + .is_some()); + assert_eq!(dyn_result.extract_value_dyn(json!(7)), json!(9)); +} diff --git a/src/unit_tests/rules/travelingsalesman_ilp.rs b/src/unit_tests/rules/travelingsalesman_ilp.rs index acf626957..c81a477f4 100644 --- a/src/unit_tests/rules/travelingsalesman_ilp.rs +++ b/src/unit_tests/rules/travelingsalesman_ilp.rs @@ -2,7 +2,7 @@ use super::*; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; fn k4_tsp() -> TravelingSalesman { TravelingSalesman::new( @@ -43,7 +43,7 @@ fn test_reduction_c4_closed_loop() { // Verify extracted solution is valid on source problem let metric = problem.evaluate(&extracted); assert!(metric.is_valid(), "Extracted solution must be valid"); - assert_eq!(metric, SolutionSize::Valid(4)); + assert_eq!(metric, Min(Some(4))); } #[test] @@ -60,7 +60,7 @@ fn test_reduction_k4_weighted_closed_loop() { // Solve via brute force for cross-check let bf = BruteForce::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_metric = problem.evaluate(&bf_solutions[0]); let ilp_metric = problem.evaluate(&extracted); @@ -87,7 +87,7 @@ fn test_reduction_c5_unweighted_closed_loop() { let metric = problem.evaluate(&extracted); assert!(metric.is_valid()); - assert_eq!(metric, SolutionSize::Valid(5)); + assert_eq!(metric, Min(Some(5))); } #[test] @@ -144,6 +144,6 @@ fn test_solve_reduced() { // Cross-check with brute force let bf = BruteForce::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); assert_eq!(metric, problem.evaluate(&bf_solutions[0])); } diff --git a/src/unit_tests/rules/travelingsalesman_qubo.rs b/src/unit_tests/rules/travelingsalesman_qubo.rs index 7976d68c6..1095a6937 100644 --- a/src/unit_tests/rules/travelingsalesman_qubo.rs +++ b/src/unit_tests/rules/travelingsalesman_qubo.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; #[test] fn test_travelingsalesman_to_qubo_closed_loop() { @@ -12,7 +12,7 @@ fn test_travelingsalesman_to_qubo_closed_loop() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // All QUBO solutions should extract to valid TSP solutions for sol in &qubo_solutions { @@ -20,7 +20,7 @@ fn test_travelingsalesman_to_qubo_closed_loop() { let metric = tsp.evaluate(&extracted); assert!(metric.is_valid(), "Extracted solution should be valid"); // K3 has only one Hamiltonian cycle (all 3 edges), cost = 1+2+3 = 6 - assert_eq!(metric, SolutionSize::Valid(6)); + assert_eq!(metric, Min(Some(6))); } // There are multiple QUBO optima (different position assignments for the same tour), @@ -40,14 +40,14 @@ fn test_travelingsalesman_to_qubo_k4() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // Every Hamiltonian cycle in K4 uses exactly 4 edges, so cost = 4 for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); let metric = tsp.evaluate(&extracted); assert!(metric.is_valid(), "Extracted solution should be valid"); - assert_eq!(metric, SolutionSize::Valid(4)); + assert_eq!(metric, Min(Some(4))); } // K4 has 3 distinct Hamiltonian cycles, but each has multiple position encodings diff --git a/src/unit_tests/solvers/brute_force.rs b/src/unit_tests/solvers/brute_force.rs index 2e80628ef..75d31d717 100644 --- a/src/unit_tests/solvers/brute_force.rs +++ b/src/unit_tests/solvers/brute_force.rs @@ -1,75 +1,64 @@ use super::*; use crate::solvers::Solver; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::{Max, Min, Or, Sum}; -// Simple maximization problem #[derive(Clone)] -struct MaxSumOpt { +struct MaxSumProblem { weights: Vec, } -impl Problem for MaxSumOpt { - const NAME: &'static str = "MaxSumOpt"; - type Metric = SolutionSize; +impl Problem for MaxSumProblem { + const NAME: &'static str = "MaxSumProblem"; + type Value = Max; + fn dims(&self) -> Vec { vec![2; self.weights.len()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { - SolutionSize::Valid( + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Max(Some( config .iter() .zip(&self.weights) .map(|(&c, &w)| if c == 1 { w } else { 0 }) .sum(), - ) + )) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "i32")] } } -impl OptimizationProblem for MaxSumOpt { - type Value = i32; - fn direction(&self) -> Direction { - Direction::Maximize - } -} - -// Simple minimization problem #[derive(Clone)] -struct MinSumOpt { +struct MinSumProblem { weights: Vec, } -impl Problem for MinSumOpt { - const NAME: &'static str = "MinSumOpt"; - type Metric = SolutionSize; +impl Problem for MinSumProblem { + const NAME: &'static str = "MinSumProblem"; + type Value = Min; + fn dims(&self) -> Vec { vec![2; self.weights.len()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { - SolutionSize::Valid( + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Min(Some( config .iter() .zip(&self.weights) .map(|(&c, &w)| if c == 1 { w } else { 0 }) .sum(), - ) + )) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "i32")] } } -impl OptimizationProblem for MinSumOpt { - type Value = i32; - fn direction(&self) -> Direction { - Direction::Minimize - } -} - -// Satisfaction problem (Metric = bool) #[derive(Clone)] struct SatProblem { num_vars: usize, @@ -78,165 +67,133 @@ struct SatProblem { impl Problem for SatProblem { const NAME: &'static str = "SatProblem"; - type Metric = bool; + type Value = Or; + fn dims(&self) -> Vec { vec![2; self.num_vars] } - fn evaluate(&self, config: &[usize]) -> bool { - self.satisfying.iter().any(|s| s == config) + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Or(self.satisfying.iter().any(|s| s == config)) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "bool")] } } -#[test] -fn test_solver_maximization() { - let problem = MaxSumOpt { - weights: vec![1, 2, 3], - }; - let solver = BruteForce::new(); - - let best = solver.find_all_best(&problem); - assert_eq!(best.len(), 1); - assert_eq!(best[0], vec![1, 1, 1]); // Select all for max sum = 6 +#[derive(Clone)] +struct SumProblem { + weights: Vec, } -#[test] -fn test_solver_minimization() { - let problem = MinSumOpt { - weights: vec![1, 2, 3], - }; - let solver = BruteForce::new(); - - let best = solver.find_all_best(&problem); - assert_eq!(best.len(), 1); - assert_eq!(best[0], vec![0, 0, 0]); // Select none for min sum = 0 -} +impl Problem for SumProblem { + const NAME: &'static str = "SumProblem"; + type Value = Sum; -#[test] -fn test_solver_multiple_optimal() { - // Two variables with equal weights -> multiple optima - let problem = MaxSumOpt { - weights: vec![5, 5], - }; - let solver = BruteForce::new(); - - let best = solver.find_all_best(&problem); - assert_eq!(best.len(), 1); - assert_eq!(best[0], vec![1, 1]); // Only one optimal: select both = 10 -} + fn dims(&self) -> Vec { + vec![2; self.weights.len()] + } -#[test] -fn test_solver_empty() { - let problem = MaxSumOpt { weights: vec![] }; - let solver = BruteForce::new(); + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config + .iter() + .zip(&self.weights) + .map(|(&c, &w)| if c == 1 { w } else { 0 }) + .sum()) + } - let best = solver.find_all_best(&problem); - assert_eq!(best, vec![Vec::::new()]); + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "u64")] + } } #[test] -fn test_solver_find_satisfying() { - let problem = SatProblem { - num_vars: 2, - satisfying: vec![vec![1, 0], vec![0, 1]], +fn test_solver_solves_max_value() { + let problem = MaxSumProblem { + weights: vec![1, 2, 3], }; let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); - assert!(solution.is_some()); - let sol = solution.unwrap(); - assert!(problem.evaluate(&sol)); + assert_eq!(solver.solve(&problem), Max(Some(6))); } #[test] -fn test_solver_find_satisfying_unsat() { - let problem = SatProblem { - num_vars: 2, - satisfying: vec![], // No satisfying assignment +fn test_solver_solves_min_value() { + let problem = MinSumProblem { + weights: vec![1, 2, 3], }; let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); - assert!(solution.is_none()); + assert_eq!(solver.solve(&problem), Min(Some(0))); } #[test] -fn test_solver_find_all_satisfying() { +fn test_solver_solves_satisfaction_value() { let problem = SatProblem { num_vars: 2, satisfying: vec![vec![1, 0], vec![0, 1]], }; let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); - assert_eq!(solutions.len(), 2); - assert!(solutions.contains(&vec![1, 0])); - assert!(solutions.contains(&vec![0, 1])); + assert_eq!(solver.solve(&problem), Or(true)); } #[test] -fn test_solver_find_satisfying_empty_dims_satisfiable() { - let problem = SatProblem { - num_vars: 0, - satisfying: vec![vec![]], +fn test_solver_find_witness() { + let problem = MaxSumProblem { + weights: vec![1, 2, 3], }; let solver = BruteForce::new(); - assert_eq!(solver.find_satisfying(&problem), Some(vec![])); - assert_eq!( - solver.find_all_satisfying(&problem), - vec![Vec::::new()] - ); + assert_eq!(solver.find_witness(&problem), Some(vec![1, 1, 1])); } #[test] -fn test_solver_find_satisfying_empty_dims_unsat() { +fn test_solver_find_witness_for_satisfaction_problem() { let problem = SatProblem { - num_vars: 0, - satisfying: vec![], + num_vars: 2, + satisfying: vec![vec![1, 0], vec![0, 1]], }; let solver = BruteForce::new(); - assert_eq!(solver.find_satisfying(&problem), None); - assert_eq!( - solver.find_all_satisfying(&problem), - Vec::>::new() - ); + let witness = solver.find_witness(&problem); + assert!(witness.is_some()); + assert_eq!(problem.evaluate(&witness.unwrap()), Or(true)); } #[test] -fn test_find_best_returns_one_solution() { - let problem = MaxSumOpt { +fn test_solver_find_witness_returns_none_for_sum_problem() { + let problem = SumProblem { weights: vec![1, 2, 3], }; let solver = BruteForce::new(); - let best = solver.find_best(&problem); - assert!(best.is_some()); - assert_eq!(best.unwrap(), vec![1, 1, 1]); + assert_eq!(solver.find_witness(&problem), None); } #[test] -fn test_find_best_empty_problem() { - let problem = MaxSumOpt { weights: vec![] }; +fn test_solver_find_all_witnesses() { + let problem = SatProblem { + num_vars: 2, + satisfying: vec![vec![1, 0], vec![0, 1]], + }; let solver = BruteForce::new(); - let best = solver.find_best(&problem); - assert_eq!(best, Some(vec![])); + let witnesses = solver.find_all_witnesses(&problem); + assert_eq!(witnesses.len(), 2); + assert!(witnesses.contains(&vec![1, 0])); + assert!(witnesses.contains(&vec![0, 1])); } #[test] -fn test_find_best_minimization() { - let problem = MinSumOpt { +fn test_solver_find_all_witnesses_returns_empty_for_sum_problem() { + let problem = SumProblem { weights: vec![1, 2, 3], }; let solver = BruteForce::new(); - let best = solver.find_best(&problem); - assert!(best.is_some()); - assert_eq!(best.unwrap(), vec![0, 0, 0]); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] @@ -245,15 +202,14 @@ fn test_solver_with_real_mis() { use crate::topology::SimpleGraph; use crate::traits::Problem; - // Triangle graph: MIS = 1 let problem = MaximumIndependentSet::new( SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), vec![1i32; 3], ); let solver = BruteForce::new(); - let best = solver.find_all_best(&problem); - assert_eq!(best.len(), 3); // Three single-vertex solutions + let best = solver.find_all_witnesses(&problem); + assert_eq!(best.len(), 3); for sol in &best { assert_eq!(sol.iter().sum::(), 1); assert!(problem.evaluate(sol).is_valid()); @@ -265,16 +221,49 @@ fn test_solver_with_real_sat() { use crate::models::formula::{CNFClause, Satisfiability}; use crate::traits::Problem; - // (x1 OR x2) AND (NOT x1 OR NOT x2) let problem = Satisfiability::new( 2, vec![CNFClause::new(vec![1, 2]), CNFClause::new(vec![-1, -2])], ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 2); for sol in &solutions { assert!(problem.evaluate(sol)); } } + +#[test] +fn test_solve_with_witnesses_max() { + let problem = MaxSumProblem { + weights: vec![1, 2, 3], + }; + let solver = BruteForce::new(); + + let (value, witnesses) = solver.solve_with_witnesses(&problem); + assert_eq!(value, Max(Some(6))); + assert_eq!(witnesses, vec![vec![1, 1, 1]]); +} + +#[test] +fn test_solve_with_witnesses_sum_returns_empty() { + let problem = SumProblem { + weights: vec![1, 2], + }; + let solver = BruteForce::new(); + + let (value, witnesses) = solver.solve_with_witnesses(&problem); + assert_eq!(value, Sum(6)); // 0+0 + 0+2 + 1+0 + 1+2 = 6 + assert!(witnesses.is_empty()); +} + +#[test] +fn test_solver_trait_solve() { + let problem = MaxSumProblem { + weights: vec![1, 2, 3], + }; + let solver = BruteForce::new(); + + assert_eq!(Solver::solve(&solver, &problem), Max(Some(6))); +} diff --git a/src/unit_tests/solvers/ilp/solver.rs b/src/unit_tests/solvers/ilp/solver.rs index f4da81fa8..02f527ede 100644 --- a/src/unit_tests/solvers/ilp/solver.rs +++ b/src/unit_tests/solvers/ilp/solver.rs @@ -69,7 +69,7 @@ fn test_ilp_solver_matches_brute_force() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&ilp); + let bf_solutions = bf.find_all_witnesses(&ilp); let ilp_solution = ilp_solver.solve(&ilp).unwrap(); // Both should find optimal value (2) @@ -207,7 +207,7 @@ fn test_ilp_multiple_constraints() { // Check against brute force let bf = BruteForce::new(); - let bf_solutions = bf.find_all_best(&ilp); + let bf_solutions = bf.find_all_witnesses(&ilp); let bf_size = ilp.evaluate(&bf_solutions[0]).unwrap(); assert!( @@ -251,3 +251,127 @@ fn test_ilp_with_time_limit() { let solution = solver.solve(&ilp); assert!(solution.is_some()); } + +#[test] +fn test_ilp_solve_via_reduction_success() { + use crate::models::graph::MaximumIndependentSet; + use crate::topology::SimpleGraph; + use std::collections::BTreeMap; + + let solver = ILPSolver::new(); + let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); + let variant = BTreeMap::from([ + ("graph".to_string(), "SimpleGraph".to_string()), + ("weight".to_string(), "i32".to_string()), + ]); + let result = solver.try_solve_via_reduction("MaximumIndependentSet", &variant, &problem); + assert!(result.is_ok()); + let sol = result.unwrap(); + let eval = problem.evaluate(&sol); + assert!(eval.is_valid()); +} + +#[test] +fn test_ilp_solve_via_reduction_no_path() { + use std::collections::BTreeMap; + + // Use a problem name that doesn't exist in the graph + let solver = ILPSolver::new(); + let ilp = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, 1.0), (1, 1.0)], 1.0)], + vec![(0, 1.0)], + ObjectiveSense::Maximize, + ); + // solve_via_reduction on an ILP itself should succeed directly + let result = solver.try_solve_via_reduction( + "ILP", + &BTreeMap::from([("type".to_string(), "bool".to_string())]), + &ilp, + ); + assert!(result.is_ok()); +} + +#[test] +fn test_ilp_solve_dyn_bool() { + let solver = ILPSolver::new(); + let ilp = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, 1.0), (1, 1.0)], 1.0)], + vec![(0, 1.0), (1, 2.0)], + ObjectiveSense::Maximize, + ); + let result = solver.solve_dyn(&ilp as &dyn std::any::Any); + assert!(result.is_some()); +} + +#[test] +fn test_ilp_solve_dyn_i32() { + let solver = ILPSolver::new(); + let ilp = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, 1.0)], 3.0)], + vec![(0, 1.0), (1, 1.0)], + ObjectiveSense::Maximize, + ); + let result = solver.solve_dyn(&ilp as &dyn std::any::Any); + assert!(result.is_some()); +} + +#[test] +fn test_ilp_solve_dyn_unknown_type_returns_none() { + let solver = ILPSolver::new(); + let not_ilp: i32 = 42; + let result = solver.solve_dyn(¬_ilp as &dyn std::any::Any); + assert!(result.is_none()); +} + +#[test] +fn test_ilp_supports_direct_dyn() { + let solver = ILPSolver::new(); + let ilp_bool = ILP::::empty(); + let ilp_i32 = ILP::::new(1, vec![], vec![], ObjectiveSense::Maximize); + let not_ilp: i32 = 42; + + assert!(solver.supports_direct_dyn(&ilp_bool as &dyn std::any::Any)); + assert!(solver.supports_direct_dyn(&ilp_i32 as &dyn std::any::Any)); + assert!(!solver.supports_direct_dyn(¬_ilp as &dyn std::any::Any)); +} + +#[test] +fn test_solve_via_reduction_error_display() { + use crate::solvers::ilp::SolveViaReductionError; + + let err = SolveViaReductionError::WitnessPathRequired { + name: "Foo".to_string(), + }; + assert!(err.to_string().contains("witness-capable")); + assert!(err.to_string().contains("Foo")); + + let err = SolveViaReductionError::NoReductionPath { + name: "Bar".to_string(), + }; + assert!(err.to_string().contains("No reduction path")); + assert!(err.to_string().contains("Bar")); + + let err = SolveViaReductionError::NoSolution { + name: "Baz".to_string(), + }; + assert!(err.to_string().contains("no solution")); + assert!(err.to_string().contains("Baz")); + + // std::error::Error is implemented + let _: &dyn std::error::Error = &err; +} + +#[test] +fn test_solve_via_reduction_returns_none_for_no_path() { + let solver = ILPSolver::new(); + let not_ilp: i32 = 42; + let result = solver.solve_via_reduction( + "NonexistentProblem", + &std::collections::BTreeMap::new(), + ¬_ilp as &dyn std::any::Any, + ); + assert!(result.is_none()); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index f45862e05..7e0fbba89 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -22,7 +22,6 @@ fn check_problem_trait(problem: &P, name: &str) { ); } } - #[test] fn test_all_problems_implement_trait_correctly() { check_problem_trait( @@ -237,79 +236,3 @@ fn test_all_problems_implement_trait_correctly() { "ConsecutiveOnesSubmatrix", ); } - -#[test] -fn test_direction() { - use crate::traits::OptimizationProblem; - use crate::types::Direction; - - // Minimization problems - assert_eq!( - MinimumVertexCover::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), - Direction::Minimize - ); - assert_eq!( - MinimumDominatingSet::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), - Direction::Minimize - ); - assert_eq!( - MinimumSetCovering::::new(2, vec![vec![0, 1]]).direction(), - Direction::Minimize - ); - assert_eq!( - PaintShop::new(vec!["a", "a"]).direction(), - Direction::Minimize - ); - assert_eq!( - QUBO::from_matrix(vec![vec![1.0]]).direction(), - Direction::Minimize - ); - assert_eq!( - SpinGlass::new(1, vec![], vec![0.0]).direction(), - Direction::Minimize - ); - assert_eq!( - BMF::new(vec![vec![true]], 1).direction(), - Direction::Minimize - ); - assert_eq!(Factoring::new(6, 2, 2).direction(), Direction::Minimize); - assert_eq!( - BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0)]), 1).direction(), - Direction::Minimize - ); - assert_eq!( - QuadraticAssignment::new(vec![vec![0, 1], vec![1, 0]], vec![vec![0, 1], vec![1, 0]]) - .direction(), - Direction::Minimize - ); - - // Maximization problems - assert_eq!( - MaximumIndependentSet::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), - Direction::Maximize - ); - assert_eq!( - MaximalIS::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), - Direction::Maximize - ); - assert_eq!( - MaxCut::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32]).direction(), - Direction::Maximize - ); - assert_eq!( - MaximumMatching::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32]).direction(), - Direction::Maximize - ); - assert_eq!( - MaximumSetPacking::::new(vec![vec![0]]).direction(), - Direction::Maximize - ); - assert_eq!( - MaximumClique::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), - Direction::Maximize - ); - assert_eq!( - PartiallyOrderedKnapsack::new(vec![2, 3], vec![3, 2], vec![(0, 1)], 5).direction(), - Direction::Maximize - ); -} diff --git a/src/unit_tests/traits.rs b/src/unit_tests/traits.rs index 6fadbaa2c..cedb4325b 100644 --- a/src/unit_tests/traits.rs +++ b/src/unit_tests/traits.rs @@ -1,7 +1,5 @@ -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; - -// === Problem trait tests === +use crate::traits::Problem; +use crate::types::{Max, Min, Or, Sum}; #[derive(Clone)] struct TestSatProblem { @@ -11,13 +9,16 @@ struct TestSatProblem { impl Problem for TestSatProblem { const NAME: &'static str = "TestSat"; - type Metric = bool; + type Value = Or; + fn dims(&self) -> Vec { vec![2; self.num_vars] } - fn evaluate(&self, config: &[usize]) -> bool { - self.satisfying.iter().any(|s| s == config) + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Or(self.satisfying.iter().any(|s| s == config)) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "bool")] } @@ -29,9 +30,10 @@ fn test_problem_sat() { num_vars: 2, satisfying: vec![vec![1, 0], vec![0, 1]], }; + assert_eq!(p.dims(), vec![2, 2]); - assert!(p.evaluate(&[1, 0])); - assert!(!p.evaluate(&[0, 0])); + assert_eq!(p.evaluate(&[1, 0]), Or(true)); + assert_eq!(p.evaluate(&[0, 0]), Or(false)); } #[test] @@ -40,6 +42,7 @@ fn test_problem_num_variables() { num_vars: 5, satisfying: vec![], }; + assert_eq!(p.num_variables(), 5); assert_eq!(p.dims().len(), 5); } @@ -50,12 +53,11 @@ fn test_problem_empty() { num_vars: 0, satisfying: vec![], }; + assert_eq!(p.num_variables(), 0); assert!(p.dims().is_empty()); } -// === OptimizationProblem trait tests === - #[derive(Clone)] struct TestMaxProblem { weights: Vec, @@ -63,31 +65,27 @@ struct TestMaxProblem { impl Problem for TestMaxProblem { const NAME: &'static str = "TestMax"; - type Metric = SolutionSize; + type Value = Max; + fn dims(&self) -> Vec { vec![2; self.weights.len()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { - SolutionSize::Valid( + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Max(Some( config .iter() .enumerate() .map(|(i, &v)| if v == 1 { self.weights[i] } else { 0 }) .sum(), - ) + )) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "i32")] } } -impl OptimizationProblem for TestMaxProblem { - type Value = i32; - fn direction(&self) -> Direction { - Direction::Maximize - } -} - #[derive(Clone)] struct TestMinProblem { costs: Vec, @@ -95,54 +93,48 @@ struct TestMinProblem { impl Problem for TestMinProblem { const NAME: &'static str = "TestMin"; - type Metric = SolutionSize; + type Value = Min; + fn dims(&self) -> Vec { vec![2; self.costs.len()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { - SolutionSize::Valid( + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Min(Some( config .iter() .enumerate() .map(|(i, &v)| if v == 1 { self.costs[i] } else { 0 }) .sum(), - ) + )) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "i32")] } } -impl OptimizationProblem for TestMinProblem { - type Value = i32; - fn direction(&self) -> Direction { - Direction::Minimize - } -} - #[test] -fn test_optimization_problem_maximize() { +fn test_problem_max_value() { let p = TestMaxProblem { weights: vec![3, 1, 4], }; - assert_eq!(p.evaluate(&[1, 0, 1]), SolutionSize::Valid(7)); - assert_eq!(p.evaluate(&[0, 0, 0]), SolutionSize::Valid(0)); - assert_eq!(p.evaluate(&[1, 1, 1]), SolutionSize::Valid(8)); - assert_eq!(p.direction(), Direction::Maximize); + + assert_eq!(p.evaluate(&[1, 0, 1]), Max(Some(7))); + assert_eq!(p.evaluate(&[0, 0, 0]), Max(Some(0))); + assert_eq!(p.evaluate(&[1, 1, 1]), Max(Some(8))); } #[test] -fn test_optimization_problem_minimize() { +fn test_problem_min_value() { let p = TestMinProblem { costs: vec![5, 2, 3], }; - assert_eq!(p.evaluate(&[1, 0, 0]), SolutionSize::Valid(5)); - assert_eq!(p.evaluate(&[0, 1, 1]), SolutionSize::Valid(5)); - assert_eq!(p.evaluate(&[0, 0, 0]), SolutionSize::Valid(0)); - assert_eq!(p.direction(), Direction::Minimize); -} -// === Multi-dimension (non-binary) problems === + assert_eq!(p.evaluate(&[1, 0, 0]), Min(Some(5))); + assert_eq!(p.evaluate(&[0, 1, 1]), Min(Some(5))); + assert_eq!(p.evaluate(&[0, 0, 0]), Min(Some(0))); +} #[derive(Clone)] struct MultiDimProblem { @@ -151,13 +143,16 @@ struct MultiDimProblem { impl Problem for MultiDimProblem { const NAME: &'static str = "MultiDim"; - type Metric = i32; + type Value = Sum; + fn dims(&self) -> Vec { self.dims.clone() } - fn evaluate(&self, config: &[usize]) -> i32 { - config.iter().map(|&c| c as i32).sum() + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config.iter().map(|&c| c as i32).sum()) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "i32")] } @@ -165,18 +160,16 @@ impl Problem for MultiDimProblem { #[test] fn test_multi_dim_problem() { - // 3 variables with cardinalities [2, 3, 4] let p = MultiDimProblem { dims: vec![2, 3, 4], }; + assert_eq!(p.dims(), vec![2, 3, 4]); assert_eq!(p.num_variables(), 3); - assert_eq!(p.evaluate(&[0, 0, 0]), 0); - assert_eq!(p.evaluate(&[1, 2, 3]), 6); + assert_eq!(p.evaluate(&[0, 0, 0]), Sum(0)); + assert_eq!(p.evaluate(&[1, 2, 3]), Sum(6)); } -// === Problem NAME constant === - #[test] fn test_problem_name() { assert_eq!(TestSatProblem::NAME, "TestSat"); @@ -185,8 +178,6 @@ fn test_problem_name() { assert_eq!(MultiDimProblem::NAME, "MultiDim"); } -// === Problem with f64 metric === - #[derive(Clone)] struct FloatProblem { weights: Vec, @@ -194,44 +185,38 @@ struct FloatProblem { impl Problem for FloatProblem { const NAME: &'static str = "FloatProblem"; - type Metric = SolutionSize; + type Value = Max; + fn dims(&self) -> Vec { vec![2; self.weights.len()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { - SolutionSize::Valid( + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Max(Some( config .iter() .enumerate() .map(|(i, &v)| if v == 1 { self.weights[i] } else { 0.0 }) .sum(), - ) + )) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "f64")] } } -impl OptimizationProblem for FloatProblem { - type Value = f64; - fn direction(&self) -> Direction { - Direction::Maximize - } -} - #[test] -fn test_float_metric_problem() { +fn test_float_value_problem() { let p = FloatProblem { weights: vec![1.5, 2.5, 3.0], }; + assert_eq!(p.dims(), vec![2, 2, 2]); - assert!((p.evaluate(&[1, 1, 0]).unwrap() - 4.0).abs() < 1e-10); - assert!((p.evaluate(&[1, 1, 1]).unwrap() - 7.0).abs() < 1e-10); - assert_eq!(p.direction(), Direction::Maximize); + assert!((p.evaluate(&[1, 1, 0]).0.unwrap() - 4.0).abs() < 1e-10); + assert!((p.evaluate(&[1, 1, 1]).0.unwrap() - 7.0).abs() < 1e-10); } -// === Catalog bridge === - #[test] fn problem_type_bridge_returns_catalog_entry_for_registered_type() { use crate::models::graph::MaximumIndependentSet; @@ -243,8 +228,6 @@ fn problem_type_bridge_returns_catalog_entry_for_registered_type() { assert!(!pt.dimensions.is_empty()); } -// === Clone constraint === - #[test] fn test_problem_is_clone() { let p1 = TestSatProblem { @@ -252,6 +235,7 @@ fn test_problem_is_clone() { satisfying: vec![vec![1, 0]], }; let p2 = p1.clone(); + assert_eq!(p2.dims(), vec![2, 2]); - assert!(p2.evaluate(&[1, 0])); + assert_eq!(p2.evaluate(&[1, 0]), Or(true)); } diff --git a/src/unit_tests/types.rs b/src/unit_tests/types.rs index 663fee0db..e0f9d01f9 100644 --- a/src/unit_tests/types.rs +++ b/src/unit_tests/types.rs @@ -1,41 +1,130 @@ use super::*; +use crate::types::Aggregate; #[test] -fn test_solution_size_valid() { - let size: SolutionSize = SolutionSize::Valid(42); +fn test_max_identity_and_combine() { + assert_eq!(Max::::identity(), Max(None)); + assert_eq!(Max(Some(7)).combine(Max(Some(3))), Max(Some(7))); + assert_eq!(Max(Some(3)).combine(Max(Some(7))), Max(Some(7))); + assert_eq!(Max::::identity().combine(Max(Some(5))), Max(Some(5))); +} + +#[test] +fn test_min_identity_and_combine() { + assert_eq!(Min::::identity(), Min(None)); + assert_eq!(Min(Some(3)).combine(Min(Some(7))), Min(Some(3))); + assert_eq!(Min(Some(7)).combine(Min(Some(3))), Min(Some(3))); + assert_eq!(Min::::identity().combine(Min(Some(5))), Min(Some(5))); +} + +#[test] +fn test_sum_identity_and_combine() { + assert_eq!(Sum::::identity(), Sum(0)); + assert_eq!(Sum(4_u64).combine(Sum(3_u64)), Sum(7)); +} + +#[test] +fn test_or_identity_and_combine() { + assert_eq!(Or::identity(), Or(false)); + assert_eq!(Or(false).combine(Or(true)), Or(true)); + assert_eq!(Or(false).combine(Or(false)), Or(false)); +} + +#[test] +fn test_and_identity_and_combine() { + assert_eq!(And::identity(), And(true)); + assert_eq!(And(true).combine(And(false)), And(false)); + assert_eq!(And(true).combine(And(true)), And(true)); +} + +#[test] +fn test_sum_witness_defaults() { + assert!(!Sum::::supports_witnesses()); + assert!(!Sum::::contributes_to_witnesses(&Sum(3), &Sum(7))); +} + +#[test] +fn test_and_witness_defaults() { + assert!(!And::supports_witnesses()); + assert!(!And::contributes_to_witnesses(&And(true), &And(true))); +} + +#[test] +fn test_max_witness_hooks() { + assert!(Max::::supports_witnesses()); + assert!(Max::contributes_to_witnesses(&Max(Some(7)), &Max(Some(7)))); + assert!(!Max::contributes_to_witnesses(&Max(Some(3)), &Max(Some(7)))); + assert!(!Max::contributes_to_witnesses(&Max(None), &Max(Some(7)))); +} + +#[test] +fn test_min_witness_hooks() { + assert!(Min::::supports_witnesses()); + assert!(Min::contributes_to_witnesses(&Min(Some(3)), &Min(Some(3)))); + assert!(!Min::contributes_to_witnesses(&Min(Some(7)), &Min(Some(3)))); + assert!(!Min::contributes_to_witnesses(&Min(None), &Min(Some(3)))); +} + +#[test] +fn test_or_witness_hooks() { + assert!(Or::supports_witnesses()); + assert!(Or::contributes_to_witnesses(&Or(true), &Or(true))); + assert!(!Or::contributes_to_witnesses(&Or(false), &Or(true))); + assert!(!Or::contributes_to_witnesses(&Or(true), &Or(false))); +} + +#[test] +fn test_max_helpers() { + let size = Max(Some(42)); assert!(size.is_valid()); assert_eq!(size.size(), Some(&42)); + assert_eq!(size.unwrap(), 42); } #[test] -fn test_solution_size_invalid() { - let size: SolutionSize = SolutionSize::Invalid; +fn test_max_invalid() { + let size = Max::(None); assert!(!size.is_valid()); assert_eq!(size.size(), None); } #[test] -fn test_solution_size_unwrap() { - let valid: SolutionSize = SolutionSize::Valid(10); - assert_eq!(valid.unwrap(), 10); +#[should_panic(expected = "called unwrap on invalid Max value")] +fn test_max_unwrap_panics() { + let invalid = Max::(None); + invalid.unwrap(); } #[test] -#[should_panic(expected = "called unwrap on Invalid")] -fn test_solution_size_unwrap_panics() { - let invalid: SolutionSize = SolutionSize::Invalid; - invalid.unwrap(); +fn test_min_helpers() { + let size = Min(Some(10)); + assert!(size.is_valid()); + assert_eq!(size.size(), Some(&10)); + assert_eq!(size.unwrap(), 10); } #[test] -fn test_solution_size_map() { - let valid: SolutionSize = SolutionSize::Valid(10); - let mapped = valid.map(|x| x * 2); - assert_eq!(mapped, SolutionSize::Valid(20)); +#[should_panic(expected = "called unwrap on invalid Min value")] +fn test_min_unwrap_panics() { + let invalid = Min::(None); + invalid.unwrap(); +} - let invalid: SolutionSize = SolutionSize::Invalid; - let mapped_invalid = invalid.map(|x| x * 2); - assert_eq!(mapped_invalid, SolutionSize::Invalid); +#[test] +fn test_extremum_helpers() { + let max = Extremum::maximize(Some(10)); + assert!(max.is_valid()); + assert_eq!(max.size(), Some(&10)); + assert_eq!(max.sense, ExtremumSense::Maximize); + + let min = Extremum::minimize(Some(5)); + assert!(min.is_valid()); + assert_eq!(min.size(), Some(&5)); + assert_eq!(min.sense, ExtremumSense::Minimize); + + let invalid = Extremum::::minimize(None); + assert!(!invalid.is_valid()); + assert_eq!(invalid.size(), None); } #[test] @@ -67,16 +156,6 @@ fn test_one_json() { assert_eq!(parsed, vec![One, One]); } -#[test] -fn test_direction() { - let max_dir = Direction::Maximize; - let min_dir = Direction::Minimize; - - assert_eq!(max_dir, Direction::Maximize); - assert_eq!(min_dir, Direction::Minimize); - assert_ne!(max_dir, min_dir); -} - #[test] fn test_problem_size() { let ps = ProblemSize::new(vec![("vertices", 10), ("edges", 20)]); @@ -140,48 +219,95 @@ fn test_weight_element_f64() { } #[test] -fn test_is_better_maximize_valid_vs_valid() { - // For maximization: larger is better - let a = SolutionSize::Valid(10); - let b = SolutionSize::Valid(5); - assert!(a.is_better(&b, Direction::Maximize)); - assert!(!b.is_better(&a, Direction::Maximize)); +fn test_extremum_aggregate_identity_and_combine() { + // identity is Maximize(None) + let id = Extremum::::identity(); + assert_eq!(id.sense, ExtremumSense::Maximize); + assert_eq!(id.value, None); + + // None + Some => Some (takes rhs sense) + let combined = Extremum::::identity().combine(Extremum::maximize(Some(5))); + assert_eq!(combined, Extremum::maximize(Some(5))); + + // Some + None => Some (keeps lhs sense) + let combined = Extremum::minimize(Some(3)).combine(Extremum::::identity()); + assert_eq!(combined, Extremum::minimize(Some(3))); + + // Maximize: keeps the larger + let combined = Extremum::maximize(Some(3)).combine(Extremum::maximize(Some(7))); + assert_eq!(combined, Extremum::maximize(Some(7))); + let combined = Extremum::maximize(Some(7)).combine(Extremum::maximize(Some(3))); + assert_eq!(combined, Extremum::maximize(Some(7))); + + // Minimize: keeps the smaller + let combined = Extremum::minimize(Some(3)).combine(Extremum::minimize(Some(7))); + assert_eq!(combined, Extremum::minimize(Some(3))); + let combined = Extremum::minimize(Some(7)).combine(Extremum::minimize(Some(3))); + assert_eq!(combined, Extremum::minimize(Some(3))); +} + +#[test] +fn test_extremum_witness_hooks() { + assert!(Extremum::::supports_witnesses()); + + // Matching value and sense -> contributes + assert!(Extremum::contributes_to_witnesses( + &Extremum::maximize(Some(10)), + &Extremum::maximize(Some(10)), + )); + + // Different value -> does not contribute + assert!(!Extremum::contributes_to_witnesses( + &Extremum::maximize(Some(5)), + &Extremum::maximize(Some(10)), + )); + + // None config -> does not contribute + assert!(!Extremum::contributes_to_witnesses( + &Extremum::::maximize(None), + &Extremum::maximize(Some(10)), + )); +} + +#[test] +fn test_extremum_display() { + assert_eq!(format!("{}", Extremum::maximize(Some(42))), "Max(42)"); + assert_eq!(format!("{}", Extremum::::maximize(None)), "Max(None)"); + assert_eq!(format!("{}", Extremum::minimize(Some(7))), "Min(7)"); + assert_eq!(format!("{}", Extremum::::minimize(None)), "Min(None)"); +} + +#[test] +#[should_panic(expected = "called unwrap on invalid Extremum value")] +fn test_extremum_unwrap_panics() { + Extremum::::minimize(None).unwrap(); +} + +#[test] +fn test_max_display() { + assert_eq!(format!("{}", Max(Some(42))), "Max(42)"); + assert_eq!(format!("{}", Max::(None)), "Max(None)"); } #[test] -fn test_is_better_minimize_valid_vs_valid() { - // For minimization: smaller is better - let a = SolutionSize::Valid(5); - let b = SolutionSize::Valid(10); - assert!(a.is_better(&b, Direction::Minimize)); - assert!(!b.is_better(&a, Direction::Minimize)); +fn test_min_display() { + assert_eq!(format!("{}", Min(Some(7))), "Min(7)"); + assert_eq!(format!("{}", Min::(None)), "Min(None)"); } #[test] -fn test_is_better_valid_vs_invalid() { - // Valid is always better than invalid - let valid = SolutionSize::Valid(0); - let invalid: SolutionSize = SolutionSize::Invalid; - assert!(valid.is_better(&invalid, Direction::Maximize)); - assert!(valid.is_better(&invalid, Direction::Minimize)); - assert!(!invalid.is_better(&valid, Direction::Maximize)); - assert!(!invalid.is_better(&valid, Direction::Minimize)); +fn test_sum_display() { + assert_eq!(format!("{}", Sum(56_u64)), "Sum(56)"); } #[test] -fn test_is_better_invalid_vs_invalid() { - // Neither invalid is better - let a: SolutionSize = SolutionSize::Invalid; - let b: SolutionSize = SolutionSize::Invalid; - assert!(!a.is_better(&b, Direction::Maximize)); - assert!(!a.is_better(&b, Direction::Minimize)); +fn test_or_display() { + assert_eq!(format!("{}", Or(true)), "Or(true)"); + assert_eq!(format!("{}", Or(false)), "Or(false)"); } #[test] -fn test_is_better_equal_valid() { - // Equal values: neither is better - let a = SolutionSize::Valid(5); - let b = SolutionSize::Valid(5); - assert!(!a.is_better(&b, Direction::Maximize)); - assert!(!a.is_better(&b, Direction::Minimize)); +fn test_and_display() { + assert_eq!(format!("{}", And(true)), "And(true)"); + assert_eq!(format!("{}", And(false)), "And(false)"); } diff --git a/tests/suites/integration.rs b/tests/suites/integration.rs index 973333f38..9a27e15df 100644 --- a/tests/suites/integration.rs +++ b/tests/suites/integration.rs @@ -23,7 +23,7 @@ mod all_problems_solvable { vec![1i32; 4], ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -37,7 +37,7 @@ mod all_problems_solvable { vec![1i32; 4], ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -51,7 +51,7 @@ mod all_problems_solvable { vec![1, 2, 1], ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); } @@ -59,8 +59,8 @@ mod all_problems_solvable { fn test_coloring_solvable() { let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); let solver = BruteForce::new(); - // KColoring returns bool, so we can use find_all_satisfying - let satisfying = solver.find_all_satisfying(&problem); + // KColoring uses the witness-capable `Or` aggregate, so all witnesses are valid colorings. + let satisfying = solver.find_all_witnesses(&problem); assert!(!satisfying.is_empty()); for sol in &satisfying { assert!(problem.evaluate(sol)); @@ -74,7 +74,7 @@ mod all_problems_solvable { vec![1i32; 4], ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -88,7 +88,7 @@ mod all_problems_solvable { vec![1i32; 4], ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -102,7 +102,7 @@ mod all_problems_solvable { vec![1, 2, 1], ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -133,7 +133,7 @@ mod all_problems_solvable { 8, ); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } @@ -146,9 +146,9 @@ mod all_problems_solvable { 2, ); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&problem); + let satisfying = solver.find_all_witnesses(&problem); assert_eq!(satisfying, vec![vec![0, 0, 1]]); - assert!(satisfying.iter().all(|config| problem.evaluate(config))); + assert!(satisfying.iter().all(|config| problem.evaluate(config).0)); } #[test] @@ -157,13 +157,13 @@ mod all_problems_solvable { 3, vec![CNFClause::new(vec![1, 2]), CNFClause::new(vec![-1, 3])], ); - // Satisfiability returns bool, find satisfying configs manually + // Satisfiability uses `Or`, so any config with `evaluate(config).0` is a witness. let dims = problem.dims(); let all_configs: Vec> = problemreductions::config::DimsIterator::new(dims.clone()).collect(); let satisfying: Vec> = all_configs .into_iter() - .filter(|config| problem.evaluate(config)) + .filter(|config| problem.evaluate(config).0) .collect(); assert!(!satisfying.is_empty()); for sol in &satisfying { @@ -175,7 +175,7 @@ mod all_problems_solvable { fn test_spin_glass_solvable() { let problem = SpinGlass::new(3, vec![((0, 1), -1.0), ((1, 2), 1.0)], vec![0.5, -0.5, 0.0]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); } @@ -187,7 +187,7 @@ mod all_problems_solvable { vec![0.0, 0.0, 1.0], ]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); } @@ -196,7 +196,7 @@ mod all_problems_solvable { let problem = MinimumSetCovering::::new(5, vec![vec![0, 1, 2], vec![2, 3, 4], vec![0, 4]]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -208,7 +208,7 @@ mod all_problems_solvable { let problem = MaximumSetPacking::::new(vec![vec![0, 1], vec![2, 3], vec![1, 2], vec![4]]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -222,13 +222,13 @@ mod all_problems_solvable { BooleanExpr::and(vec![BooleanExpr::var("x"), BooleanExpr::var("y")]), )]); let problem = CircuitSAT::new(circuit); - // CircuitSAT returns bool + // CircuitSAT also uses `Or`, so witness enumeration lines up with configs where `.0` is true. let dims = problem.dims(); let all_configs: Vec> = problemreductions::config::DimsIterator::new(dims.clone()).collect(); let satisfying: Vec> = all_configs .into_iter() - .filter(|config| problem.evaluate(config)) + .filter(|config| problem.evaluate(config).0) .collect(); assert!(!satisfying.is_empty()); for sol in &satisfying { @@ -240,7 +240,7 @@ mod all_problems_solvable { fn test_factoring_solvable() { let problem = Factoring::new(15, 2, 2); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -255,7 +255,7 @@ mod all_problems_solvable { vec![0, 1, 2], ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.contains(&vec![1, 0, 0])); } @@ -263,7 +263,7 @@ mod all_problems_solvable { fn test_paintshop_solvable() { let problem = PaintShop::new(vec!["a", "b", "a", "b"]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); } @@ -275,7 +275,7 @@ mod all_problems_solvable { 1, ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -286,7 +286,7 @@ mod all_problems_solvable { fn test_bmf_solvable() { let problem = BMF::new(vec![vec![true, true], vec![true, true]], 1); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { // BMF minimizes Hamming distance, all configs are valid (no invalid marker) @@ -311,8 +311,8 @@ mod problem_relationships { let vc_problem = MinimumVertexCover::new(SimpleGraph::new(n, edges), vec![1i32; n]); let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(&is_problem); - let vc_solutions = solver.find_all_best(&vc_problem); + let is_solutions = solver.find_all_witnesses(&is_problem); + let vc_solutions = solver.find_all_witnesses(&vc_problem); let max_is_size = is_solutions[0].iter().sum::(); let min_vc_size = vc_solutions[0].iter().sum::(); @@ -331,7 +331,7 @@ mod problem_relationships { let is_problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges), vec![1i32; n]); let solver = BruteForce::new(); - let maximal_solutions = solver.find_all_best(&maximal_is); + let maximal_solutions = solver.find_all_witnesses(&maximal_is); // Every maximal IS is also a valid IS for sol in &maximal_solutions { @@ -367,7 +367,7 @@ mod problem_relationships { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Optimal should be all same spin (all 0 or all 1) for sol in &solutions { @@ -391,11 +391,11 @@ mod problem_relationships { let solver = BruteForce::new(); // All sets needed for cover - let cover_solutions = solver.find_all_best(&covering); + let cover_solutions = solver.find_all_witnesses(&covering); assert_eq!(cover_solutions[0].iter().sum::(), 3); // All sets can be packed (no overlap) - let pack_solutions = solver.find_all_best(&packing); + let pack_solutions = solver.find_all_witnesses(&packing); assert_eq!(pack_solutions[0].iter().sum::(), 3); } } @@ -408,7 +408,7 @@ mod edge_cases { fn test_empty_graph_independent_set() { let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![]), vec![1i32; 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // All vertices can be in IS when no edges assert_eq!(solutions[0].iter().sum::(), 3); @@ -420,7 +420,7 @@ mod edge_cases { let edges = vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]; let problem = MaximumIndependentSet::new(SimpleGraph::new(4, edges), vec![1i32; 4]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Maximum IS in complete graph is 1 assert_eq!(solutions[0].iter().sum::(), 1); @@ -435,7 +435,7 @@ mod edge_cases { problemreductions::config::DimsIterator::new(dims.clone()).collect(); let satisfying: Vec> = all_configs .into_iter() - .filter(|config| problem.evaluate(config)) + .filter(|config| problem.evaluate(config).0) .collect(); // (x1 OR NOT x2) is satisfied by 3 of 4 assignments @@ -450,7 +450,7 @@ mod edge_cases { // Factor 4 = 2 * 2 let problem = Factoring::new(4, 2, 2); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { @@ -462,7 +462,7 @@ mod edge_cases { fn test_single_car_paintshop() { let problem = PaintShop::new(vec!["a", "a"]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Single car always has 1 switch (color must change) assert_eq!(problem.count_switches(&solutions[0]), 1); @@ -478,7 +478,7 @@ mod weighted_problems { let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![10, 1, 1]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Should prefer vertex 0 (weight 10) over vertex 1 (weight 1) // Optimal: {0, 2} with weight 11 @@ -496,7 +496,7 @@ mod weighted_problems { MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 10, 1]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Prefer {0, 2} over {1} because {0,2} has weight 2 vs {1} has weight 10 let best_weight: i32 = solutions[0] @@ -511,7 +511,7 @@ mod weighted_problems { fn test_weighted_max_cut() { let problem = MaxCut::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![10, 1]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Maximum cut should include the heavy edge (0,1) let cut_value = problem.evaluate(&solutions[0]); @@ -536,7 +536,7 @@ mod weighted_problems { problemreductions::config::DimsIterator::new(dims.clone()).collect(); let satisfying: Vec> = all_configs .into_iter() - .filter(|config| problem.evaluate(config)) + .filter(|config| problem.evaluate(config).0) .collect(); // Can't satisfy both - no solution satisfies all clauses diff --git a/tests/suites/reductions.rs b/tests/suites/reductions.rs index 66e81ae6e..5ea6e9ecd 100644 --- a/tests/suites/reductions.rs +++ b/tests/suites/reductions.rs @@ -31,7 +31,7 @@ mod is_vc_reductions { // Solve the target VC problem let solver = BruteForce::new(); - let vc_solutions = solver.find_all_best(vc_problem); + let vc_solutions = solver.find_all_witnesses(vc_problem); // Extract back to IS solution let is_solution = result.extract_solution(&vc_solutions[0]); @@ -58,7 +58,7 @@ mod is_vc_reductions { // Solve the target IS problem let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(is_problem); + let is_solutions = solver.find_all_witnesses(is_problem); // Extract back to VC solution let vc_solution = result.extract_solution(&is_solutions[0]); @@ -91,7 +91,7 @@ mod is_vc_reductions { // Solve the final problem let solver = BruteForce::new(); - let solutions = solver.find_all_best(final_is); + let solutions = solver.find_all_witnesses(final_is); // Extract through the chain let intermediate_sol = back_to_is.extract_solution(&solutions[0]); @@ -126,10 +126,10 @@ mod is_vc_reductions { let solver = BruteForce::new(); // Solve IS, reduce to VC solution - let is_solutions = solver.find_all_best(&is_problem); + let is_solutions = solver.find_all_witnesses(&is_problem); let max_is = is_solutions[0].iter().sum::(); - let vc_solutions = solver.find_all_best(&vc_problem); + let vc_solutions = solver.find_all_witnesses(&vc_problem); let min_vc = vc_solutions[0].iter().sum::(); assert_eq!(max_is + min_vc, n); @@ -156,7 +156,7 @@ mod is_sp_reductions { // Solve let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(sp_problem); + let sp_solutions = solver.find_all_witnesses(sp_problem); // Extract to IS solution let is_solution = result.extract_solution(&sp_solutions[0]); @@ -178,7 +178,7 @@ mod is_sp_reductions { // Solve let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(is_problem); + let is_solutions = solver.find_all_witnesses(is_problem); // Extract to SP solution let sp_solution = result.extract_solution(&is_solutions[0]); @@ -201,7 +201,7 @@ mod is_sp_reductions { // Solve SP let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(sp_problem); + let sp_solutions = solver.find_all_witnesses(sp_problem); // Extract to IS solution let is_solution = to_sp.extract_solution(&sp_solutions[0]); @@ -210,7 +210,7 @@ mod is_sp_reductions { assert!(original.evaluate(&is_solution).is_valid()); // Should match directly solving IS - let direct_solutions = solver.find_all_best(&original); + let direct_solutions = solver.find_all_witnesses(&original); let direct_max = direct_solutions[0].iter().sum::(); let reduced_max = is_solution.iter().sum::(); @@ -234,7 +234,7 @@ mod sg_qubo_reductions { // Solve QUBO let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // Extract to SG solution let sg_solution = result.extract_solution(&qubo_solutions[0]); @@ -253,7 +253,7 @@ mod sg_qubo_reductions { // Solve SG let solver = BruteForce::new(); - let sg_solutions = solver.find_all_best(sg); + let sg_solutions = solver.find_all_witnesses(sg); // Extract to QUBO solution let qubo_solution = result.extract_solution(&sg_solutions[0]); @@ -275,8 +275,8 @@ mod sg_qubo_reductions { // Check that ground states correspond let solver = BruteForce::new(); - let sg_solutions = solver.find_all_best(&sg); - let qubo_solutions = solver.find_all_best(qubo); + let sg_solutions = solver.find_all_witnesses(&sg); + let qubo_solutions = solver.find_all_witnesses(qubo); // Extract QUBO solution back to SG let extracted = result.extract_solution(&qubo_solutions[0]); @@ -316,7 +316,7 @@ mod sg_maxcut_reductions { // Solve MaxCut let solver = BruteForce::new(); - let maxcut_solutions = solver.find_all_best(maxcut); + let maxcut_solutions = solver.find_all_witnesses(maxcut); // Extract to SG solution let sg_solution = result.extract_solution(&maxcut_solutions[0]); @@ -338,7 +338,7 @@ mod sg_maxcut_reductions { // Solve SG let solver = BruteForce::new(); - let sg_solutions = solver.find_all_best(sg); + let sg_solutions = solver.find_all_witnesses(sg); // Extract to MaxCut solution let maxcut_solution = result.extract_solution(&sg_solutions[0]); @@ -360,8 +360,8 @@ mod sg_maxcut_reductions { let solver = BruteForce::new(); // Solve both - let sg_solutions = solver.find_all_best(&sg); - let maxcut_solutions = solver.find_all_best(maxcut); + let sg_solutions = solver.find_all_witnesses(&sg); + let maxcut_solutions = solver.find_all_witnesses(maxcut); // Extract MaxCut solution back to SG let extracted = result.extract_solution(&maxcut_solutions[0]); @@ -389,7 +389,7 @@ mod topology_tests { let sp = MaximumSetPacking::::new(vec![vec![0, 1, 2], vec![2, 3], vec![3, 4]]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&sp); + let solutions = solver.find_all_witnesses(&sp); assert!(sp.evaluate(&solutions[0]).is_valid()); } @@ -410,7 +410,7 @@ mod topology_tests { let is_problem = MaximumIndependentSet::new(SimpleGraph::new(4, edges), vec![1i32; 4]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&is_problem); + let solutions = solver.find_all_witnesses(&is_problem); // Vertices 0-1 are connected, 2-3 are connected // Max IS: {0, 2} or {0, 3} or {1, 2} or {1, 3} = size 2 @@ -479,7 +479,7 @@ mod qubo_reductions { assert_eq!(qubo.num_variables(), data.qubo_num_vars); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); // All QUBO optimal solutions should extract to valid IS solutions for sol in &solutions { @@ -524,7 +524,7 @@ mod qubo_reductions { assert_eq!(qubo.num_variables(), data.qubo_num_vars); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); for sol in &solutions { let extracted = reduction.extract_solution(sol); @@ -561,7 +561,7 @@ mod qubo_reductions { assert_eq!(qubo.num_variables(), data.qubo_num_vars); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); for sol in &solutions { let extracted = reduction.extract_solution(sol); @@ -626,7 +626,7 @@ mod qubo_reductions { assert_eq!(qubo.num_variables(), data.qubo_num_vars); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); for sol in &solutions { let extracted = reduction.extract_solution(sol); @@ -710,7 +710,7 @@ mod qubo_reductions { assert!(qubo.num_variables() >= data.qubo_num_vars); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); for sol in &solutions { let extracted = reduction.extract_solution(sol); @@ -778,7 +778,7 @@ mod qubo_reductions { let qubo: &QUBO = chain.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); // Extract back through the full chain to get VC solution for sol in &solutions { @@ -866,20 +866,20 @@ mod end_to_end { // Solve directly let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(&is); + let is_solutions = solver.find_all_witnesses(&is); let direct_size = is_solutions[0].iter().sum::(); // Reduce to VC and solve let to_vc = ReduceTo::>::reduce_to(&is); let vc = to_vc.target_problem(); - let vc_solutions = solver.find_all_best(vc); + let vc_solutions = solver.find_all_witnesses(vc); let vc_extracted = to_vc.extract_solution(&vc_solutions[0]); let via_vc_size = vc_extracted.iter().sum::(); // Reduce to MaximumSetPacking and solve let to_sp = ReduceTo::>::reduce_to(&is); let sp = to_sp.target_problem(); - let sp_solutions = solver.find_all_best(sp); + let sp_solutions = solver.find_all_witnesses(sp); let sp_extracted = to_sp.extract_solution(&sp_solutions[0]); let via_sp_size = sp_extracted.iter().sum::(); @@ -899,7 +899,7 @@ mod end_to_end { // Solve directly let solver = BruteForce::new(); - let sg_solutions = solver.find_all_best(&sg); + let sg_solutions = solver.find_all_witnesses(&sg); // Convert usize solution to i32 spin values for compute_energy let direct_spins: Vec = sg_solutions[0].iter().map(|&x| x as i32).collect(); @@ -908,7 +908,7 @@ mod end_to_end { // Reduce to MaxCut and solve let to_maxcut = ReduceTo::>::reduce_to(&sg); let maxcut = to_maxcut.target_problem(); - let maxcut_solutions = solver.find_all_best(maxcut); + let maxcut_solutions = solver.find_all_witnesses(maxcut); let maxcut_extracted = to_maxcut.extract_solution(&maxcut_solutions[0]); // Convert extracted solution to spins for energy computation @@ -935,7 +935,7 @@ mod end_to_end { // Solve VC let solver = BruteForce::new(); - let vc_solutions = solver.find_all_best(vc); + let vc_solutions = solver.find_all_witnesses(vc); // Extract back through chain let is_sol = is_to_vc.extract_solution(&vc_solutions[0]);