diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 438584ded..c8eb87e49 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -37,7 +37,7 @@ These repo-local skills live under `.claude/skills/*/SKILL.md`. - [propose](skills/propose/SKILL.md) -- Interactive brainstorming to help domain experts propose a new model or rule. Asks one question at a time, uses mathematical language (no programming jargon), and files a GitHub issue. - [final-review](skills/final-review/SKILL.md) -- Interactive maintainer review for PRs in "Final review" column. Merges main, walks through agentic review bullets with human, then merge or hold. - [dev-setup](skills/dev-setup/SKILL.md) -- Interactive wizard to install and configure all development tools for new maintainers. -- [verify-reduction](skills/verify-reduction/SKILL.md) -- Standalone mathematical verification of a reduction rule: Typst proof, constructor Python (≥5000 checks), adversary Python (≥5000 independent checks). Reports verdict, no artifacts saved. Also called as a subroutine by `/add-rule` (default behavior). +- [verify-reduction](skills/verify-reduction/SKILL.md) -- Standalone mathematical verification of a reduction rule: Typst proof, constructor Python, and independent adversary Python with coverage justified by the construction. Reports verdict, no artifacts saved. Also called as a subroutine by `/add-rule` (default behavior). - [update-papers](skills/update-papers/SKILL.md) -- Update research paper collection: download new papers from references.bib, retry failed downloads, sync to Google Drive, regenerate index.md. - [find-solver](skills/find-solver/SKILL.md) -- Interactive guide: match a real-world problem to a library model, explore reduction paths, recommend solvers (built-in + external), and generate a solution doc. - [find-problem](skills/find-problem/SKILL.md) -- Reverse of find-solver: given a solver for a model, discover what other problems it can handle via incoming reductions, ranked by effective complexity. @@ -107,7 +107,7 @@ make papers-pull # Pull PDFs from shared remote - 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/models/decision.rs` - Generic `Decision

` wrapper converting optimization problems to decision problems -- `src/solvers/` - BruteForce reference solver returning problem solutions, ILP solver (feature-gated), decision search (binary search via Decision queries), and the exact-variant solver capability registry. Solver dispatch uses only registered customized implementations and fixed ILP pipelines; reduction-graph reachability does not imply solver availability. Run `pred inspect ` to see the registered capabilities for that instance. +- `src/solvers/` - BruteForce reference solver returning problem solutions, ILP solver, decision search (binary search via Decision queries), and the exact-variant solver capability registry. Solver dispatch uses only registered customized implementations and fixed ILP pipelines; reduction-graph reachability does not imply solver availability. Run `pred inspect ` to see the registered capabilities for that instance. - `src/traits.rs` - `Problem` trait - `src/rules/traits.rs` - `ReduceTo`, `ReduceToAggregate`, `ReductionResult`, `AggregateReductionResult` traits - `src/registry/` - Compile-time reduction metadata collection @@ -134,7 +134,7 @@ Problem (core trait — all problems must implement) ``` `BruteForceProblem` is a separate reference-solver capability. Its -`dimensions()` method describes only the finite Cartesian coordinate space used +fallible `num_variables()` and `dimension(variable)` methods describe only the finite Cartesian coordinate space used by the registered brute-force implementation. **Objective problems** (e.g., `MaximumIndependentSet`) typically use `Value = Max`, `Min`, or `Extremum`. @@ -161,13 +161,15 @@ Max, Min, Sum, Or, And, Extremum, ExtremumSense - `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 load/serialize/solution-solve metadata. One entry per problem may be marked `default`, and variable names in complexity strings are validated against the problem-owned parameter schema. Ordinary models are constructed directly from their construction schema. When user-facing construction differs from persisted JSON, define a model-local `#[derive(CreateSpec)]` DTO plus `TryFrom`, use its generated `FIELDS` in `ProblemSchemaEntry`, and register it with `create LocalSpec`; never add model-name branches in CLI or MCP code. - `decision_problem_meta!` macro registers `DecisionProblemMeta` for a concrete inner type, providing the `DECISION_NAME` constant. -- `register_decision_variant!` macro generates `declare_variants!`, `ProblemSchemaEntry`, and both `ReductionEntry` submissions (aggregate Decision→Opt + Turing Opt→Decision) for a `Decision

` variant. Callers must define inherent getters (`num_vertices()`, `num_edges()`, `k()`) on `Decision

` before invoking. Accepts an explicit structural `category` plus `dims`, `fields`, and `parameter_getters` parameters for problem-specific parameters. +- `register_decision_variant!` macro generates `declare_variants!`, `ProblemSchemaEntry`, and both `ReductionEntry` submissions (witness/aggregate Decision→Opt + Turing Opt→Decision) for a `Decision

` variant. Callers must define inherent getters (`num_vertices()`, `num_edges()`, `k()`) on `Decision

` before invoking. Accepts an explicit structural `category` plus `dims`, `fields`, and `parameter_getters` parameters for problem-specific parameters. - Problems parameterized by graph type `G` and optionally weight type `W` (problem-dependent) - `BruteForce::solve()` returns `Result, SolveError>`; `None` means exhaustive search proved infeasibility - `BruteForce::find_all_witnesses()` is a reference-testing helper for collecting every optimal or satisfying solution +- Each executed witness step constructs one result and shares its witness/value views through `Rc`. Document the rule's domain, witness premise, source guarantee, and infeasibility interpretation; all tied qualifying optima must map correctly. +- `SolutionAggregate` belongs to `solvers::BruteForce` witness selection. Models, pure reduction mappings, dynamic evaluation, and non-enumerative solving do not require it. See [executed lifecycle](../docs/src/design.md#executed-reduction-lifecycle). - `ReductionResult` provides `target_problem()` and `extract_solution()` for witness/config workflows; `AggregateReductionResult` provides `extract_value()` for aggregate/value workflows -- Every direct `extract_solution()` must call `validate_target_solution()` once before decoding; composed extractors delegate validation to the first direct decoder. -- Decode only the reduction's defined mathematical mapping. Reject malformed structure with `ExtractionError`; never panic, truncate, clamp, invent defaults, or add recovery branches. Explicit mathematical alternatives and sentinels are allowed. Test successful decoding and every rejected representation. +- Direct `extract_solution()` maps solutions under the reduction's mathematical premises. `pred extract` has the same witness precondition. Neither validates feasibility or optimality. Transport parses/types inputs; solver orchestration interprets aggregate outcomes before mapping. +- Decode only the reduction's defined mathematical mapping. Preserve reachable mathematical and representation errors; do not add fallback values or recovery branches for violations already excluded by the calling contract. Explicit mathematical alternatives and sentinels are allowed. - 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), `i64`, `f64` — all implement `WeightElement` trait @@ -210,7 +212,7 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: - Each primitive reduction is determined by the exact `(source_variant, target_variant)` endpoint pair - Reduction edges carry `EdgeCapabilities { witness, aggregate, turing }`; graph search defaults to witness mode, aggregate mode is available through `ReductionMode::Aggregate`, and Turing (multi-query) mode via `ReductionMode::Turing` - `#[reduction]` requires one `transform = exact`, `transform = upper_bound`, or `transform = unavailable` declaration and currently registers witness/config reductions; aggregate-only and Turing edges require manual `ReductionEntry` registration -- `Decision

→ P` is an aggregate-only edge (solve optimization, compare to bound); `P → Decision

` is a Turing edge (binary search over decision bound) +- `Decision

→ P` supplies witness and aggregate operations on one result (solve optimization, compare to bound, extract when the bound is met); `P → Decision

` is a Turing edge (binary search over decision bound) ### Extension Points - New models register dynamic load/serialize metadata through `declare_variants!` and, when finite enumeration exists, register it separately through `register_brute_force!`; neither belongs in CLI match arms @@ -228,22 +230,33 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: ### Numeric Contract -Follow the [numeric types and arithmetic standard](../docs/src/design.md#numeric-types-and-arithmetic) -for every model and reduction. `usize` is reserved for in-memory indices, -collection lengths, and brute-force dimensions; canonical problem parameters -are `u64`; signed mathematical integers use `i64`; and approximate -real values use finite `f64`. Before implementation, identify each numeric -input and domain, each computed total and result type, the largest supported -value, every range/sign-changing conversion, overflow behavior, and whether -arithmetic is exact or approximate. Use `TryFrom` at range boundaries and -checked arithmetic for derived values that may overflow. Rust construction, -serde, CLI, and MCP must enforce the same range. +Follow the [numeric contract](../docs/src/design.md#numeric-types-and-arithmetic) +for models and reductions. Identify numeric domains, stored result types, +conversion boundaries, and overflow behavior before implementation. Rust +construction, serde, CLI, and MCP must use the same model validation; backend +transport limits must not narrow that model domain. Issue contributors provide the mathematical definition, domains, and constraints; implementers derive the Rust representation. Do not require issue authors to choose implementation types or add implementation-specific numeric fields to issue templates. Changes to issue templates require user approval. +### Reduction and Solver Boundary + +Follow the canonical [responsibility boundaries](../docs/src/design.md#responsibility-boundaries), +[witness/aggregate contracts](../docs/src/design.md#witness-and-aggregate-reductions), +and [validation policy](../docs/src/design.md#validation-evidence). +Models own mathematical semantics; rules own construction and witness mappings; +adapters own numerical transport, termination interpretation, and returned-target +validation. Orchestration uses those results and maps solutions under the rules' premises. +External extraction parses and types submitted witnesses and assumes the rule's mathematical premises. Solver completion interprets the rule's value relationship before mapping; extraction does not validate feasibility or optimality. Fix shared paths and update all callers rather +than adding model-specific branches or independent backend optimality checks. + +In ILP tests, only `ILPSolveError::Infeasible` means infeasibility. Other errors +must fail with their details. Solver integration failures must be distinguished +from model or reduction errors; do not require backend precision stress tests +in every rule. See the design document for the shared search-representation contract. + ### File Naming - Reduction files: `src/rules/_.rs` (e.g., `maximumindependentset_qubo.rs`) - Model files: `src/models//.rs` — category is by input structure: `graph/` (graph input), `formula/` (boolean formula/circuit), `set/` (universe + subsets), `algebraic/` (matrix/linear system/lattice), `misc/` (other) @@ -270,7 +283,7 @@ fields to issue templates. Changes to issue templates require user approval. ### Coverage -New code must have >95% test coverage. Run `make coverage` to check. +New code must have >95% test coverage. Run `make coverage` to check. This is a hard gate: do not waive it, lower the threshold, or add exclusions to make a change pass. `make coverage` checks committed and uncommitted changed lines against `origin/main` using the workspace LCOV report; set `COVERAGE_BASE` when reviewing against another base. Whole-repository coverage is a separate metric. ### Naming @@ -286,7 +299,9 @@ See Key Patterns above for solver API signatures. Follow the reference files for Unit tests in `src/unit_tests/` linked via `#[path]` (see Core Modules above). Integration tests in `tests/suites/`, consolidated through `tests/main.rs`. Canonical example-db coverage lives in `src/unit_tests/example_db.rs`. -Model review automation checks for a dedicated test file under `src/unit_tests/models/...` with at least 3 test functions. The exact split of coverage is judged per model during review. +Model review checks for a dedicated test file under `src/unit_tests/models/...` +and evaluates its semantic coverage under the [validation policy](../docs/src/design.md#validation-evidence). +Do not impose minimum test-function, assertion, vertex, or generated-check counts. ## Documentation Locations - `README.md` — Project overview and quickstart diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md index 6cf14b947..4df33fdb6 100644 --- a/.claude/skills/add-model/SKILL.md +++ b/.claude/skills/add-model/SKILL.md @@ -20,7 +20,7 @@ Before any implementation, collect all required information. If called from `iss | 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 | +| 6 | **Configuration space** | Mathematical solution representation and domain | One Boolean selection per vertex | | 7 | **Feasibility check** | How to validate a configuration | "All selected vertices must be pairwise adjacent" | | 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\|" | @@ -74,15 +74,15 @@ Read these first to understand the patterns: ## Pre-review Checklist Before implementing, make sure the plan explicitly covers these items that structural review checks later: -- Follow `docs/src/design.md#numeric-types-and-arithmetic`: `usize` is for in-memory indices, collection lengths, and brute-force dimensions; registered problem size parameters use `u64`; signed mathematical integers use `i64`; Boolean data uses `bool`; and approximate real or rational data uses finite `f64`. Use another format only when required by the mathematical problem or schema, such as `BigUint` for arbitrary-precision problems or `One` for unit weights; implementation convenience is not sufficient, and there is no `i32` model or I/O format. Implementation-local values are outside this contract. +- Read the canonical [numeric contract](../../../docs/src/design.md#numeric-types-and-arithmetic), [responsibility boundaries](../../../docs/src/design.md#responsibility-boundaries), and [validation policy](../../../docs/src/design.md#validation-evidence). Derive fields and arithmetic from the model's mathematical domain. Evaluation must not depend on solver tolerances, statuses, or enumeration cardinality. Reuse existing representations and shared APIs. - Keep failure phases explicit: fallible constructors, create specs, serde-facing validation, and random generation return `ConstructionError`; `evaluate()` returns `EvaluationError`; no public model path returns `Result<_, String>`. Stored-field arithmetic and evaluation arithmetic are checked and reported in their own phase. -- Serde/CLI construction uses the same validation as `new`/`try_new`, and boundary tests cover the supported maximum without requiring impractical allocation. +- Serde/CLI construction uses the same validation as `new`/`try_new`, and focused tests cover actual representation risks without impractical allocation or backend precision stress cases. - `ProblemSchemaEntry` metadata is complete (`display_name`, `aliases`, `dimensions`, explicit `category`, and construction `fields`) - `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` -- If the issue explicitly claims direct ILP solving, the plan also includes the direct ` -> ILP` rule with exact overhead metadata, feature-gated registration, strong regression tests, and ILP-enabled verification +- If the issue explicitly claims direct ILP solving, the plan also includes the direct ` -> ILP` rule with correct parameter metadata, registration, semantic regression tests, and representative solver integration - `docs/paper/reductions.typ` adds both the display-name dictionary entry and the `problem-def(...)` ## Step 1: Determine the category @@ -215,11 +215,11 @@ This example is now the canonical source for: If the issue explicitly says the model is solvable by reducing **directly** to ILP, implement `src/rules/_ilp.rs` in the **same PR** as the model. This is the one exception to the normal "one item per PR" policy: the direct ` -> ILP` rule is part of the model feature, not optional follow-up work. Completeness bar: -- Feature-gate the rule under `ilp-solver` and register it normally -- Add exact overhead expressions and any required size-field getters; metadata must match the constructed ILP exactly -- Add strong tests in `src/unit_tests/rules/_ilp.rs`: structure/metadata, closed-loop semantics vs the source problem or brute force, extraction, `solve_reduced()` or ILP path coverage when appropriate, and weighted/infeasible/pathological regressions whenever the model semantics admit them +- Register the native ILP rule normally; there is no ILP solver feature gate +- Declare parameter equalities or upper bounds using existing metadata; verify the relationship against the constructed ILP +- Add strong tests in `src/unit_tests/rules/_ilp.rs`: structure/metadata, closed-loop semantics vs the source problem or brute force, extraction, `solve_reduced()` or ILP path coverage when appropriate, and weighted/infeasible cases and arithmetic regressions justified by the construction - Update CLI/example-db/paper paths so the claimed ILP solver route is actually usable and documented -- Verify with ILP-enabled workspace commands, not just non-ILP unit tests +- Run the relevant solver integration tests as well as direct mathematical tests; HiGHS is a regular dependency, not an optional feature A direct ILP rule shipped with a model issue must match the completeness bar of a standalone production ILP reduction. Do not add a stub just to satisfy the issue text. @@ -227,12 +227,12 @@ A direct ILP rule shipped with a model issue must match the completeness bar of 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: +Choose coverage from the model semantics and concrete implementation risks under the canonical validation policy. There is no required test-function count: -- **Creation/basic** — exercise constructor inputs, key accessors, `dims()` / `num_variables()`. +- **Creation/basic** — exercise constructor inputs, key accessors, and the mathematical witness domain. - **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. +- **Solver** — where registered, brute-force `solve()` returns a correct solution; use `find_all_witnesses()` when the test needs all optimal/satisfying witnesses. Keep solver integration separate from direct model evaluation. - **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). @@ -298,8 +298,8 @@ make test clippy # Must pass If Step 4.7 applied, run ILP-enabled workspace verification instead: ```bash -cargo clippy --all-targets --features ilp-highs -- -D warnings -cargo test --features "ilp-highs example-db" --workspace --verbose +cargo clippy --all-targets -- -D warnings +cargo test --features example-db --workspace --verbose ``` Structural and quality review is handled by the `review-pipeline` stage, not here. The run stage just needs to produce working code. @@ -333,4 +333,4 @@ Structural and quality review is handled by the `review-pipeline` stage, not her | Calling a panicking constructor from `TryFrom` | Share a fallible constructor and preserve its `ConstructionError`. | | Missing canonical model example | Add a builder in `src/example_db/model_builders.rs` and keep it aligned with paper/example workflows | | Paper example not tested | Must include `test__paper_example` that verifies the exact instance, solution, and solution count shown in the paper | -| Claiming direct ILP solving but leaving ` -> ILP` for later | If the issue promises a direct ILP path, implement that rule in the same PR with exact overhead metadata and production-level ILP tests | +| Claiming direct ILP solving but leaving ` -> ILP` for later | If the issue promises a direct ILP path, implement that rule in the same PR with correct parameter relationships and production-level ILP tests | diff --git a/.claude/skills/add-rule/SKILL.md b/.claude/skills/add-rule/SKILL.md index 3097be526..dd54ca36b 100644 --- a/.claude/skills/add-rule/SKILL.md +++ b/.claude/skills/add-rule/SKILL.md @@ -36,47 +36,31 @@ Before any implementation, collect all required information. If called from `iss If any item is missing, ask the user to provide it. Put a high standard on item 7 (concrete example): it must be in tutorial style with clear intuition and easy to understand. Do NOT proceed until the checklist is complete. -## Step 0.5: Type Compatibility Gate - -Check source/target `Value` types before any work: - -```bash -grep "type Value = " src/models/*/.rs src/models/*/.rs -``` - -**Compatible pairs for `ReduceTo` (witness-capable):** -- `Or`->`Or`, `Min`->`Min`, `Max`->`Max` (same type) -- `Or`->`Min`, `Or`->`Max` (feasibility embeds into optimization) - -**Incompatible — STOP if any of these:** -- `Min`->`Or` or `Max`->`Or` — optimization source has no threshold K; needs a decision-variant source model -- `Max`->`Min` or `Min`->`Max` — opposite optimization directions; needs `ReduceToAggregate` or a decision-variant wrapper -- `Or`->`Sum` or `Min`->`Sum` — Sum is aggregate-only; needs `ReduceToAggregate` -- Any pair involving `And` or `Sum` on the target side - -If incompatible, STOP and comment on the issue explaining the type mismatch and options. Do NOT proceed. - -## Numeric Safety Gate - -Read `docs/src/design.md#numeric-types-and-arithmetic`. Derive implementation -types, supported ranges, and checked conversions from the mathematical source, -target, and reduction algorithm. Use `usize` for in-memory indices, collection -lengths, and brute-force dimensions; `u64` for registered problem size -parameters; `i64` for signed mathematical integers; `bool` for Boolean data; -and finite `f64` for real or rational data. Another format needs mathematical -or target-schema justification; there is no `i32` boundary format. -Temporary reduction calculations are outside this format contract, but fields -written into the target must use the target model's format. - -Ask the contributor only when a mathematical domain or constraint is ambiguous; -do not ask them to choose Rust types. Do not use `as` for range/sign changes. -Check target-size arithmetic and auxiliary identifiers before constructing the -target, verify serde/CLI uses the same ranges, and add focused boundary tests. -The public reduction returns `ReductionError`: preserve a target constructor's -`ConstructionError` as `ReductionError::Construction`, and report reduction -arithmetic directly as the corresponding `ReductionError`; do not stringify or -silently handle either error. Convert model-derived `i64` values to `f64` only -through `i64_to_exact_f64`. +## Step 0.5: Mathematical and API Contract + +Read [the canonical witness/aggregate contract](../../../docs/src/design.md#witness-and-aggregate-reductions). +Resolve the source and target's concrete `Solution` and `Value` types from their +implementations and check the construction, extraction preconditions, and +objective relationship. Different optimization directions or numeric value +types do not by themselves invalidate a witness reduction. Use the existing +witness, aggregate, or Turing capability required by the actual operation. +Report a concrete mathematical or Rust implementation mismatch if one exists; +do not apply a wrapper-pair whitelist. + +## Arithmetic and Validation + +Follow [the canonical arithmetic and boundary policy](../../../docs/src/design.md#arithmetic) +and [validation evidence](../../../docs/src/design.md#validation-evidence). +Derive representation requirements from the source, target, and construction. +Ask for clarification only when the mathematical domain is ambiguous, not to +make the contributor choose Rust types. + +Check the construction's actual size arithmetic, coefficients, and auxiliary +identifiers. Preserve target `ConstructionError` as `ReductionError::Construction` +and report reduction arithmetic through `ReductionError`; do not stringify or +silently handle failures. Reuse shared conversion and extraction APIs according +to their contracts. Backend transport limits and precision checks belong to the +adapter, not this rule's applicability domain or mandatory test template. ## Reference Implementations @@ -90,11 +74,11 @@ Read these first to understand the patterns: **If `--no-verify` was passed, skip to Step 2.** -Invoke the `/verify-reduction` skill to mathematically verify the reduction before writing Rust code. This runs the full verification pipeline: Typst proof, constructor Python script (>=5000 checks), adversary subagent (>=5000 independent checks), and cross-comparison. +Invoke the `/verify-reduction` skill to mathematically verify the reduction before writing Rust code. This runs the full verification pipeline: Typst proof, constructor Python script, independent adversary checks, and cross-comparison with coverage justified by the construction. All verification artifacts are ephemeral — they exist only in conversation context and temp files. Nothing is committed to the repository. -**If verification FAILS: STOP. Report to user. Do NOT proceed to implementation.** +**Proceed to implementation only when verification reports VERIFIED. For FAILED or INCOMPLETE, report the concrete defect or missing evidence and resolve it before implementing.** If verification passes, the verified Python `reduce()` and `extract_solution()` functions, along with the YES/NO instances, carry forward in conversation context to inform Steps 2-5. Use them as the canonical spec for the Rust implementation. @@ -106,7 +90,7 @@ Create `src/rules/_.rs` (all lowercase, no underscores between w // Required structure: // 1. ReductionResult struct (holds the target problem + mapping state) // 2. ReductionResult trait impl (target_problem + extract_solution) -// 3. #[reduction(overhead = { ... })] on ReduceTo impl +// 3. #[reduction(transform = exact { ... })] on ReduceTo impl // 4. ReduceTo trait impl (reduce_to method) // 5. #[cfg(test)] #[path = "..."] mod tests; ``` @@ -130,31 +114,30 @@ impl ReductionResult for ReductionXToY { fn target_problem(&self) -> &Self::Target { &self.target } fn extract_solution( &self, - target_solution: &[usize], - ) -> crate::rules::ExtractionResult> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + target_solution: &::Solution, + ) -> crate::rules::ExtractionResult<::Solution> { let source_solution = /* translate the verified mathematical mapping exactly */; Ok(source_solution) } } ``` -Every direct extractor must call `validate_target_solution()` once before decoding. It checks only length and value domains, not feasibility, optimality, or rule-specific structure; reject malformed structure with `ExtractionError`. +Follow the canonical [extraction contract](../../../docs/src/design.md#witness-and-aggregate-reductions). Document the mathematical premises and implement the mapping directly. The adapter accepts solver output; external callers supply witnesses under the same mathematical contract. Extraction does not validate feasibility or optimality. Do not recheck constraints or add errors for states excluded by construction. Solver orchestration uses aggregate mappings to handle required thresholds before witness extraction; do not independently certify optimality or compensate for a rule bug with source revalidation. -**ReduceTo with `#[reduction]` macro** (overhead is **required**): +**ReduceTo with `#[reduction]` macro** (a parameter relation is **required**): ```rust -#[reduction(overhead = { +#[reduction(transform = exact { field_name = "source_field", })] impl ReduceTo for SourceType { type Result = ReductionXToY; - fn reduce_to(&self) -> Self::Result { + fn reduce_to(&self) -> Result { // If Step 1 ran: translate the verified Python reduce() logic } } ``` -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]`. +Each primitive reduction is determined by the exact source/target variant pair. Keep one primitive registration per endpoint pair and declare `transform = exact`, `upper_bound`, or `unavailable` according to the actual parameter relationship; follow `.claude/CLAUDE.md` for metadata requirements. **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. @@ -162,7 +145,7 @@ Each primitive reduction is determined by the exact source/target variant pair. Add to `src/rules/mod.rs`: - `mod _;` -- If feature-gated (e.g., ILP): wrap with `#[cfg(feature = "ilp-solver")]` +- Register native ILP rules normally; there is no ILP solver feature gate. ## Step 4: Write unit tests @@ -171,20 +154,20 @@ Create `src/unit_tests/rules/_.rs`: **Required: closed-loop test** (`test__to__closed_loop`): ```rust // 1. Create source problem instance -// 2. Reduce: let reduction = ReduceTo::::reduce_to(&source); +// 2. Reduce: let reduction = ReduceTo::::reduce_to(&source).unwrap(); // 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 ``` -If Step 1 ran, use the verified YES/NO instances from conversation context to construct test cases. Include both a feasible (closed-loop) and infeasible (no witnesses) test. +If Step 1 ran, use the verified YES/NO instances from conversation context to construct test cases. Include feasible and infeasible cases when both exist; for always-feasible optimization models, check the objective relationship instead. Additional recommended tests: - Verify target problem structure (correct size, edges, constraints) - Edge cases (empty graph, single vertex, etc.) - Weight preservation (if applicable) -Test every malformed representation distinguished by the decoder (for example, zero or multiple one-hot selections, or duplicate permutation entries). The canonical example supplies shared wrong-length and out-of-domain tests. +Test the mathematical mapping for witnesses satisfying its premises, including all tied optima on suitable small instances. Malformed witnesses do not impose rejection requirements on extraction. Keep necessary parsing/type-conversion tests at the transport boundary. For aggregate-only reductions, replace the closed-loop witness test with value-chain tests: - Solve the target with `Solver::solve()` @@ -195,7 +178,7 @@ Link via `#[cfg(test)] #[path = "..."] mod tests;` at the bottom of the rule fil ## Step 5: Add canonical example -Define `canonical_rule_example_specs()` in the rule module and include it from `src/rules/mod.rs::canonical_rule_example_specs()`. This enrolls the rule in shared round-trip, wrong-length, and out-of-domain extraction tests. +Define `canonical_rule_example_specs()` in the rule module and include it from `src/rules/mod.rs::canonical_rule_example_specs()`. This enrolls the rule in shared example checks. Extraction correctness checks use witnesses satisfying the mapping contract; model evaluation retains its own domain checks. ## Step 6: Document in paper (MANDATORY — DO NOT SKIP) @@ -272,8 +255,8 @@ Structural and quality review is handled by the `review-pipeline` stage, not her ## Solver Rules - If the target problem already has a solver, use it directly. -- If the solving strategy requires ILP, implement the ILP reduction rule alongside (feature-gated under `ilp-solver`). -- A direct-to-ILP rule is a production reduction, not a stub. Match the completeness bar used by strong ILP reductions in this repo: exact overhead metadata, structure + closed-loop + extraction tests, weighted/infeasible/pathological regressions whenever the semantics require them, and ILP-enabled workspace verification. +- If the solving strategy requires ILP, implement and register the ILP reduction rule alongside. +- A direct-to-ILP rule is a production reduction, not a stub. Match the completeness bar used by strong ILP reductions in this repo: correct parameter relationships, structure + closed-loop + extraction tests, weighted/infeasible cases and arithmetic regressions justified by the construction, and representative solver integration. - When this rule is the companion to a `[Model]` issue that explicitly claims ILP solvability, it belongs in the same PR as the model. - If a custom solver is needed, implement in `src/solvers/` and document. @@ -304,10 +287,25 @@ Aggregate-only reductions currently have a narrower CLI surface: | Wrong overhead expression | Must accurately reflect the size relationship | | Adding extra reduction metadata or duplicate primitive endpoint registration | Keep one primitive registration per endpoint pair and use only the `overhead` form of `#[reduction]` | | Missing `extract_solution` mapping state | Store any index maps needed in the ReductionResult struct | -| Permissive extraction | Validate first, then map exactly or return `ExtractionError` | +| Permissive extraction | Map witnesses satisfying the documented premises directly; do not validate feasibility or optimality | | Not adding a canonical example | Add the rule-local spec and include it from `src/rules/mod.rs` | | Not regenerating reduction graph | Run `cargo run --example export_graph` after adding a rule | | Skipping Step 6 (paper documentation) | **Every rule MUST have a `reduction-rule` entry in the paper. This is mandatory, not optional. PRs without documentation will be rejected.** | | Source/target model not fully registered | Both problems must already have `ProblemSchemaEntry`, `declare_variants!`, registry aliases as needed, and a construction contract -- use `add-model` skill first | -| Treating a direct-to-ILP rule as a toy stub | Direct ILP reductions need exact overhead metadata and strong semantic regression tests, just like other production ILP rules | +| Treating a direct-to-ILP rule as a toy stub | Direct ILP reductions need correct parameter relationships and strong semantic regression tests, just like other production ILP rules | | Skipping verification for complex reductions | Verification is default for a reason — `--no-verify` is for trivial identity/complement reductions only | + +## Reduction lifecycle responsibilities + +Apply the canonical [executed lifecycle](../../../docs/src/design.md#executed-reduction-lifecycle). +State the rule's instance domain, qualifying-witness premise, source guarantee, +and infeasibility interpretation. Check every qualifying tied optimum in small +exhaustive cases where ties are relevant. A witness flag alone does not prove +complete solvability or that adjacent path premises compose. + +Construct each executed result once and share target, witness, value, and +completion state. Outcome interpretation uses the rule's mathematical relation; +ordinary extraction assumes its premises. Keep necessary dynamic/JSON conversion +and reachable representation failures, but no checked/unchecked extraction or +pure forwarding wrappers. Do not add `SolutionAggregate` bounds to models or +mathematical mappings; it belongs to brute-force witness selection. diff --git a/.claude/skills/check-issue/SKILL.md b/.claude/skills/check-issue/SKILL.md index e63957476..3f80845dd 100644 --- a/.claude/skills/check-issue/SKILL.md +++ b/.claude/skills/check-issue/SKILL.md @@ -192,10 +192,10 @@ If the algorithm is a high-level sketch rather than an implementable procedure ### 4d: Example Quality -- **Non-trivial**: Must have enough structure to exercise the reduction meaningfully (not just 2 vertices) +- **Meaningful structure**: Exercise the defining constraints or reduction gadgets; explain what an incorrect implementation would get wrong. - **Brute-force solvable**: Small enough to verify by hand or with `pred solve` - **Fully worked**: Shows the source instance, the reduction construction step by step, and the target instance — not just "apply the reduction to get..." -- **Round-trip testable**: The example must be complex enough to validate correctness via a closed-loop test: reduce the source instance → solve the target → extract the solution back → verify it is optimal for the source. A too-simple example (e.g., a single edge, a trivially satisfiable formula) can pass the round trip even with a buggy reduction. The example should have multiple feasible solutions with different objective values so that only a correct reduction maps to the true optimum. Rule of thumb: the source instance should have at least 2 suboptimal feasible solutions in addition to the optimal one. +- **Round-trip testable**: Choose examples that can expose a concrete construction, evaluation, objective-mapping, or extraction defect under the [canonical validation policy](../../../docs/src/design.md#validation-evidence). Explain the expected outcome independently of backend success. There is no fixed number of feasible alternatives that establishes correctness. --- @@ -242,7 +242,7 @@ Read the `size_fields` and any variant getters, then enumerate corner cases the | Set systems | empty universe, empty subsets, identical subsets, universe element appearing in no subset | | Algebraic | zero matrix, identity, singular matrix | -Then trace the **issue's** algorithm by hand against at least 2 corner cases that are not the worked example: +Then trace the **issue's** algorithm by hand on relevant corner cases beyond the worked example, chosen to test concrete assumptions: 1. Pick a corner case from the table above that the source model actually allows. 2. Simulate the issue's construction step by step. @@ -404,13 +404,13 @@ The formal definition must be **precise and implementable**: ### 4d: Example Quality -- **Non-trivial**: Enough vertices/variables to exercise constraints meaningfully (not just a triangle) +- **Meaningful structure**: Exercise the defining constraints or reduction gadgets; explain what an incorrect implementation would get wrong. - **Exercises core structure**: Examples must use the defining features of the problem. For instance, a "MultivariateQuadratic" example that only has linear terms does not exercise the quadratic structure → **Fail**. If the problem's name or definition highlights a specific structural feature (quadratic, k-colorable, bipartite, etc.), at least one example must exercise that feature. - **Expected outcome provided**: - Satisfaction problems must include a concrete valid / satisfying solution and say why it is valid - Optimization problems must include a concrete optimal solution and the optimal objective value - **Detailed enough for paper**: This example will appear in the paper — it needs to be illustrative -- **Round-trip testable**: The example must be complex enough that a round-trip test (construct instance → solve → verify) can catch implementation bugs. A too-simple instance (e.g., 2 vertices, a single clause) may have a trivially correct solution that passes even with a wrong implementation. The example should have multiple feasible configurations with different objective values (for optimization) or a mix of satisfying and non-satisfying configurations (for satisfaction problems), so that correctness is meaningfully tested. Rule of thumb: the instance should have at least 2 suboptimal feasible solutions in addition to the optimal one. +- **Round-trip testable**: Choose examples that can expose a concrete construction, evaluation, objective-mapping, or extraction defect under the [canonical validation policy](../../../docs/src/design.md#validation-evidence). Explain the expected outcome independently of backend success. There is no fixed number of feasible alternatives that establishes correctness. - **ILP-testable when claimed**: If the issue advertises a direct ILP path, the example should be rich enough to support strong ILP closed-loop tests rather than a degenerate "any formulation passes" case. ### 4e: Representation Feasibility diff --git a/.claude/skills/dev-setup/SKILL.md b/.claude/skills/dev-setup/SKILL.md index 333a693e0..f8039e9de 100644 --- a/.claude/skills/dev-setup/SKILL.md +++ b/.claude/skills/dev-setup/SKILL.md @@ -123,7 +123,7 @@ This runs `fmt-check + clippy + test`. Print a pass/fail summary for each stage. | Failure | Fix | |---------|-----| | `fmt-check` fails | Run `make fmt` to auto-fix | -| Linker errors in clippy/test | Missing C/C++ toolchain for `ilp-highs` feature. Install Xcode CLT (`xcode-select --install` on macOS) or `build-essential` (`sudo apt install build-essential` on Linux) | +| Linker errors in clippy/test | Missing C/C++ toolchain required by the HiGHS backend. Install Xcode CLT (`xcode-select --install` on macOS) or `build-essential` (`sudo apt install build-essential` on Linux) | | "HiGHS not found" or cmake errors | Install cmake: `brew install cmake` (macOS) or `sudo apt install cmake` (Linux) | | `cargo llvm-cov` fails with "missing llvm-profdata" | `rustup component add llvm-tools-preview` | diff --git a/.claude/skills/review-quality/SKILL.md b/.claude/skills/review-quality/SKILL.md index 7c43c8241..aeec3ad0d 100644 --- a/.claude/skills/review-quality/SKILL.md +++ b/.claude/skills/review-quality/SKILL.md @@ -67,13 +67,18 @@ Only check these if the diff touches `problemreductions-cli/`: ## Step 5: Evaluate Test Quality +Read the canonical [validation policy](../../../docs/src/design.md#validation-evidence). Flag tests that: -- **Only check types/shapes, not values**: e.g., `assert!(result.is_some())` without checking the solution is correct -- **Mirror the implementation**: Tests recomputing the same formula as the code prove nothing -- **Lack adversarial cases**: Only happy path. Tests must include infeasible configs and boundary cases -- **Use trivial instances only**: Single-edge or 2-node tests may pass with bugs. Need 5+ vertex instances -- **Closed-loop without verification**: Must verify extracted solution is **optimal** (compare brute-force on both source and target) -- **Assert count too low**: 1-2 asserts for non-trivial code is insufficient + +- Check only types/shapes when the behavior requires a semantic value or witness assertion. +- Mirror the implementation without an independent expected result. +- Miss a concrete construction branch, infeasible configuration, or representation risk relevant to the change. +- Fail to check the reduction's stated mapping or objective relationship. Use explicit witnesses or small exhaustive oracles as appropriate; arbitrary feasible witnesses need not be optimal. +- Treat backend failures as mathematical counterexamples, or relax tolerances to make integration tests pass. +- Duplicate shared backend decoding/precision checks across rules. + +Use the smallest instances that distinguish correct from incorrect behavior. +Judge assertions by what they establish, not vertex, assertion, or test counts. ## Output Format @@ -110,3 +115,18 @@ Flag tests that: ### Summary - [list of all ISSUE items as bullet points with severity] ``` + +## Reduction lifecycle responsibilities + +Apply the canonical [executed lifecycle](../../../docs/src/design.md#executed-reduction-lifecycle). +State the rule's instance domain, qualifying-witness premise, source guarantee, +and infeasibility interpretation. Check every qualifying tied optimum in small +exhaustive cases where ties are relevant. A witness flag alone does not prove +complete solvability or that adjacent path premises compose. + +Construct each executed result once and share target, witness, value, and +completion state. Outcome interpretation uses the rule's mathematical relation; +ordinary extraction assumes its premises. Keep necessary dynamic/JSON conversion +and reachable representation failures, but no checked/unchecked extraction or +pure forwarding wrappers. Do not add `SolutionAggregate` bounds to models or +mathematical mappings; it belongs to brute-force witness selection. diff --git a/.claude/skills/review-structural/SKILL.md b/.claude/skills/review-structural/SKILL.md index d291a7e02..bf6609374 100644 --- a/.claude/skills/review-structural/SKILL.md +++ b/.claude/skills/review-structural/SKILL.md @@ -57,7 +57,7 @@ Only run if review type includes "model". Given: problem name `P`, category `C`, | 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 | +| 8 | Semantic test coverage | Inspect cases against the [validation policy](../../../docs/src/design.md#validation-evidence); require meaningful coverage of the changed behavior, not a test-function count. | | 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 | Variant registration exists | `Grep("declare_variants!|VariantEntry", file)` | @@ -66,7 +66,7 @@ Only run if review type includes "model". Given: problem name `P`, category `C`, | 14 | Canonical model example registered | `Grep("{P}", "src/example_db/model_builders.rs")` | | 15 | Paper `display-name` entry | `Grep('"{P}"', "docs/paper/reductions.typ")` | | 16 | Paper `problem-def` block | `Grep('problem-def.*"{P}"', "docs/paper/reductions.typ")` | -| 17 | Numeric and error contracts | Derive the expected boundary representation from the mathematical definition, then compare schema types, Rust fields, aggregate/total type, constructor and serde validation, conversions, overflow behavior, and boundary tests against `docs/src/design.md#numeric-types-and-arithmetic`. Verify construction paths return `ConstructionError`, `evaluate()` returns `EvaluationError`, and no public model path returns `Result<_, String>`. | +| 17 | Numeric and error contracts | Read the canonical [responsibility and arithmetic contract](../../../docs/src/design.md#responsibility-boundaries). Check model representation, constructor/serde consistency, and actual arithmetic risks; flag backend tolerances or enumeration limits used as model semantics. Verify construction paths return `ConstructionError`, `evaluate()` returns `EvaluationError`, and no public model path returns `Result<_, String>`. | ### Rule Checklist @@ -85,8 +85,8 @@ Only run if review type includes "rule". Given: source `S`, target `T`, rule fil | 9 | Canonical rule example registered | `Grep("canonical_rule_example_specs", rule file)` and verify it is included by `src/rules/mod.rs` | | 10 | Example-db lookup tests exist | `Grep("find_rule_example|build_rule_db", "src/unit_tests/example_db.rs")` | | 11 | Paper `reduction-rule` entry | `Grep('reduction-rule.*"{S}".*"{T}"', "docs/paper/reductions.typ")` | -| 12 | Extraction contract | Direct decoders call `validate_target_solution()`, enforce rule-specific structure, and test malformed cases; the helper does not establish feasibility or optimality. Composed extractors may delegate. | -| 13 | Numeric and error contracts | Compare source/target boundary types, size arithmetic, coefficients, bounds, auxiliary IDs, conversions, overflow behavior, and boundary tests against `docs/src/design.md#numeric-types-and-arithmetic`. Verify public reduction paths return `ReductionError`, preserve target `ConstructionError` as its construction cause, and never stringify or silently handle either failure. | +| 12 | Extraction contract | Follow the canonical responsibility boundaries. Both external extraction and internal rule mappings rely on documented premises; parsing and type conversion stay at the transport boundary. Reject repeated feasibility checks and error branches excluded by construction. Solver orchestration interprets aggregate thresholds to determine source answers. No independent optimality certification is required. | +| 13 | Numeric and error contracts | Check the [witness/aggregate contract](../../../docs/src/design.md#witness-and-aggregate-reductions) and actual construction arithmetic under the canonical policy. Do not reject different objective directions/value types or demand backend precision tests for every rule. Verify public reduction paths return `ReductionError`, preserve target `ConstructionError` as its construction cause, and never stringify or silently handle either failure. | ## Step 2b: Blacklisted File Check @@ -177,3 +177,18 @@ Flag any deviation as ISSUE. - X/Y issue compliance checks passed (if applicable) - [list of all FAIL/ISSUE items as bullet points] ``` + +## Reduction lifecycle responsibilities + +Apply the canonical [executed lifecycle](../../../docs/src/design.md#executed-reduction-lifecycle). +State the rule's instance domain, qualifying-witness premise, source guarantee, +and infeasibility interpretation. Check every qualifying tied optimum in small +exhaustive cases where ties are relevant. A witness flag alone does not prove +complete solvability or that adjacent path premises compose. + +Construct each executed result once and share target, witness, value, and +completion state. Outcome interpretation uses the rule's mathematical relation; +ordinary extraction assumes its premises. Keep necessary dynamic/JSON conversion +and reachable representation failures, but no checked/unchecked extraction or +pure forwarding wrappers. Do not add `SolutionAggregate` bounds to models or +mathematical mappings; it belongs to brute-force witness selection. diff --git a/.claude/skills/verify-reduction/SKILL.md b/.claude/skills/verify-reduction/SKILL.md index 91765ce3f..d0e42c3c9 100644 --- a/.claude/skills/verify-reduction/SKILL.md +++ b/.claude/skills/verify-reduction/SKILL.md @@ -1,13 +1,14 @@ --- name: verify-reduction -description: Standalone mathematical verification of a reduction rule — generates a Typst proof plus constructor and independent adversary scripts with at least 5000 checks each. Reports a verdict without saving artifacts. +description: Verify a reduction mathematically using a Typst proof and independent constructor/adversary scripts, with coverage chosen from the construction's risks. Report findings without committing artifacts. --- # Verify Reduction -Mathematical verification of a reduction rule. Produces a Typst proof + dual Python verification scripts, iterating until all checks pass. Reports a VERIFIED/FAILED verdict. All artifacts are ephemeral — nothing is committed to the repository. - -Use standalone to check correctness before implementation, or as a subroutine of `/add-rule` (which calls this by default). +Verify a reduction before implementation, standalone or as the default mathematical +verification step of `/add-rule`. Produce a proof and independent executable +checks in a temporary directory. Report what was established and any limitations; +finite checks support the proof but do not replace it. ## Invocation @@ -16,280 +17,148 @@ Use standalone to check correctness before implementation, or as a subroutine of /verify-reduction SubsetSum Partition ``` -## Step 0: Parse Input - -```bash -REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner) -ISSUE= -ISSUE_JSON=$(gh issue view "$ISSUE" --json title,body,number) -``` - -If invoked with problem names instead of an issue number, use the names directly. - -## Step 1: Read Issue, Study Models, Type Check - -```bash -gh issue view "$ISSUE" --json title,body -pred show --json -pred show --json -``` - -### Type compatibility gate — MANDATORY - -Check source/target `Value` types before any work. The `grep` only locates the definitions; it does -not resolve generic parameters or associated types: - -```bash -grep "type Value = " src/models/*/.rs src/models/*/.rs -``` - -Resolve both concrete types completely before declaring compatibility: - -1. Substitute every concrete generic argument from the proposed rule. -2. Follow every type alias and associated type to its defining `impl`. -3. Record the substitution chain and the source file evidence in the verification report. -4. If any generic or associated type remains unresolved, run a compile-backed temporary Rust probe - using `std::any::type_name::<::Value>()`. Build the probe from `/tmp` - with a path dependency on this repository; do not modify the repository. - -Never infer a Rust value type from the mathematical problem name, from unit-weight terminology, or -from the Python verifier's integer representation. In particular, arbitrary-precision Python -integers do not establish that a Rust objective type is `usize` or that it is closed under all -legal source instances. - -Required report format: +## Step 1: Read the Definition and Resolve the API + +For an issue, read it with `gh issue view --json title,body`. Inspect both +concrete models with `pred show --json` and read their implementations. +Extract the construction, mathematical domain, correctness argument, witness +mapping, parameter formulas, worked example, and references. Consult the cited +literature when needed to resolve a mathematical claim. + +Read the canonical [witness/aggregate contract](../../../docs/src/design.md#witness-and-aggregate-reductions), +[arithmetic policy](../../../docs/src/design.md#arithmetic), and +[validation policy](../../../docs/src/design.md#validation-evidence). + +Locate `Solution` and `Value` definitions with `rg`, substitute concrete generic +arguments, and follow associated types to their implementations. Record the +resolved types and source evidence. If resolution remains unclear, use a temporary +compile-backed Rust probe with a path dependency on the repository. Do not infer +Rust types from problem names, unit-weight terminology, or Python integers. + +Check the actual operation: + +- A witness reduction maps solutions and justifies feasibility and, where claimed, + optimality preservation. Different objective directions or Rust value types are + not automatic failures. For example, complementing an independent set of size + `k` gives a vertex cover of size `n-k` and reverses optimization direction. +- An aggregate reduction must justify its actual value conversion. Check the + domain of arithmetic the construction or mapping performs, not a hypothetical + conversion between all source and target objective values. +- A multi-query algorithm needs the existing Turing capability. An arbitrary + feasibility witness does not establish a source optimum without an argument. + +Report a concrete mathematical/API mismatch before implementation if one exists. +Do not replace that analysis with a wrapper-pair whitelist or backend range gate. + +## Step 2: Write the Proof + +Write a standalone Typst proof in the temporary directory containing: + +- Source/target definitions and the precise applicability domain. +- Construction steps with symbols defined before use. +- Independent forward and reverse correctness arguments. For optimization, + state the objective relationship and why target optima yield source optima. +- Witness extraction, including its mathematical preconditions. +- Target parameter formulas, distinguishing equalities from upper bounds. +- Small worked examples that exercise the construction. Include YES and NO + examples where both exist; for always-feasible optimization problems, show + the relevant objective relationship instead of inventing an infeasible case. + +Use enough detail to make the argument checkable. Do not substitute phrases such +as “obviously” or “the converse is similar” for a missing proof. Example size is +chosen for clarity and coverage, not a minimum vertex count. + +## Step 3: Implement Constructor Checks + +Write a temporary Python script with independent source/target feasibility and +objective oracles. Cover the claims relevant to this construction: + +| Claim | Evidence | +|-------|----------| +| Forward/reverse correctness | Small exhaustive instances or justified sampling; compare feasibility and the stated optimum relationship | +| Witness extraction | Target witnesses satisfying the mapping's preconditions produce valid source witnesses; check optimal mappings where claimed | +| Parameter formulas | Measure constructed targets and compare with equalities or upper bounds; use symbolic checking when it adds evidence | +| Target structure | Check the actual target invariants and gadget interactions | +| Worked examples | Reproduce the proof's values and witnesses | +| Arithmetic/case splits | Exercise concrete branches and representation risks in the construction | + +Choose exhaustive bounds and sampling from the construction's risks and cost. +Record bounds, seeds, counts, and omissions so the evidence is reproducible. +There is no universal minimum generated-check count. Do not duplicate solver +precision tests or require a backend to establish mathematical equivalence. +Python's arbitrary-precision arithmetic is not evidence that Rust construction +arithmetic cannot overflow; inspect the actual stored representation separately. + +## Step 4: Run Checks and Analyze Gaps + +Run the script and investigate failures. Correct the proof, construction, or +checker according to the evidence, then rerun affected checks. Map each proof +claim to its executable evidence or explain why it is established by proof alone. +Report untested areas rather than increasing check counts without new coverage. + +If a backend integration run is included, identify it separately. Record whether +failure occurs in construction, solving, extraction, or source validation. A +backend timeout, numerical rejection, or non-optimal termination is not itself a +counterexample to the reduction theorem and must not be reported as a pass. + +## Step 5: Independent Adversary Verification + +Dispatch an independent subagent with the problem definitions and Typst proof, +without the constructor script. Ask it to implement its own construction, +extraction, feasibility, and objective checks. It must not import the constructor +implementation. Have it challenge the proof's actual risks: + +- Complement/identity mappings: objective direction and witness correspondence. +- Algebraic mappings: case boundaries, coefficients, and extraction per case. +- Gadget mappings: unintended paths, gadget interactions, and target invariants. + +Use exhaustive checks or property-based strategies where they provide useful +independent coverage, not to satisfy a count. Reproduce applicable worked examples. +Compare both implementations on shared instances. Investigate disagreements; +structurally different but equivalent encodings may be valid. One checker passing +does not establish that the other checker is at fault. + +## Step 6: Review and Report + +Before reporting, confirm: + +- The concrete Rust types and actual witness/aggregate contract were checked. +- The proof covers construction, both directions, extraction, and parameters. +- Independent checks exercise relevant branches and mappings with reproducible + bounds/seeds; remaining gaps are stated. +- Disagreements and failures are resolved or explicitly reported. +- Mathematical evidence and backend integration results are distinguished. + +Report: ```text -TYPE RESOLUTION: - Source syntax: Min - Substitutions: W = One; ::Sum = i64 - Source resolved: Min - Target syntax: Min - Target resolved: Min - Full-domain compatibility: FAILED -``` - -**Compatible pairs for `ReduceTo` (witness-capable):** -- `Or`->`Or` -- `Min`->`Min`, `Max`->`Max` (identical resolved inner type) -- `Or`->`Min`, `Or`->`Max` (feasibility embeds into optimization) - -`Min`->`Min` or `Max`->`Max` with `S != T` is not automatically compatible. Proceed -only if the rule or source model declares a bound covering every legal source instance and the -verification proves a total, order-preserving conversion over that full declared domain. Otherwise -STOP and report a value-domain mismatch. - -**Incompatible — STOP if any of these:** -- `Min`->`Or` or `Max`->`Or` — optimization source has no threshold K; needs a decision-variant source model -- `Max`->`Min` or `Min`->`Max` — opposite optimization directions; needs `ReduceToAggregate` or a decision-variant wrapper -- `Or`->`Sum` or `Min`->`Sum` — Sum is aggregate-only; needs `ReduceToAggregate` -- Any pair involving `And` or `Sum` on the target side - -**Regression case:** `MinimumDominatingSet` resolves to `Min` because -`::Sum = i64`; `MinimumHittingSet` resolves to `Min`. Report -`Min -> Min`, not `Min -> Min`. Without an explicit source-size bound, -the full-domain type gate fails even though the classical cardinality reduction is mathematically -correct and exhaustive small-instance checks pass. - -If incompatible, STOP and report the type mismatch and options. Do NOT proceed. - -### If compatible - -Extract: construction algorithm, correctness argument, overhead formulas, worked example, reference. Use WebSearch if the issue is incomplete. - -## Step 2: Write Typst Proof - -Write a standalone Typst proof (in a temp file, not committed). - -**Mandatory structure:** - -```typst -== Source $arrow.r$ Target -#theorem[...] -#proof[ - _Construction._ (numbered steps, every symbol defined before first use) - _Correctness._ - ($arrow.r.double$) ... (genuinely independent, NOT "the converse is similar") - ($arrow.l.double$) ... - _Solution extraction._ ... -] -*Overhead.* (table with target metric -> formula) -*Feasible example.* (YES instance, >=3 variables, fully worked with numbers) -*Infeasible example.* (NO instance, fully worked — show WHY no solution exists) -``` - -**Hard rules:** -- Zero instances of "clearly", "obviously", "it is easy to see", "straightforward" -- Zero scratch work ("Wait", "Hmm", "Actually", "Let me try") -- Two examples minimum, both with >=3 variables/vertices -- Every symbol defined before first use - -## Step 3: Write Constructor Python Script - -Write a Python verification script (temp file) with ALL 7 mandatory sections: - -| Section | What to verify | Notes | -|---------|---------------|-------| -| 1. Symbolic (sympy) | Overhead formulas symbolically for general n | "The overhead is trivial" is NOT an excuse to skip | -| 2. Exhaustive forward+backward | Source feasible <=> target feasible | n <= 5 minimum. ALL instances or >=300 sampled per (n,m) | -| 3. Solution extraction | Extract source solution from every feasible target witness | Most commonly skipped section. DO NOT SKIP | -| 4. Overhead formula | Build target, measure actual size, compare against formula | Catches off-by-one in construction | -| 5. Structural properties | Target well-formed, no degenerate cases | Gadget reductions: girth, connectivity, widget structure | -| 6. YES example | Reproduce exact Typst feasible example numbers | Every value must match | -| 7. NO example | Reproduce exact Typst infeasible example, verify both sides infeasible | Must verify WHY infeasible | - -### Minimum check counts — STRICTLY ENFORCED - -| Type | Minimum checks | Minimum n | -|------|---------------|-----------| -| Identity (same graph, different objective) | 10,000 | n <= 6 | -| Algebraic (padding, complement, case split) | 10,000 | n <= 5 | -| Gadget (widget, cycle construction) | 5,000 | n <= 5 | - -Every reduction gets at least 5,000 checks regardless of perceived simplicity. - -## Step 4: Run and Iterate - -```bash -python3 /tmp/verify__.py -``` - -### Iteration 1: Fix failures - -Run the script. Fix any failures. Re-run until 0 failures. - -### Iteration 2: Check count audit - -Print and fill this table honestly: - -``` -CHECK COUNT AUDIT: - Total checks: ___ (minimum: 5,000) - Forward direction: ___ instances (minimum: all n <= 5) - Backward direction: ___ instances (minimum: all n <= 5) - Solution extraction: ___ feasible instances tested - Overhead formula: ___ instances compared - Symbolic (sympy): ___ identities verified - YES example: verified? [yes/no] - NO example: verified? [yes/no] - Structural properties: ___ checks -``` - -If ANY line is below minimum, enhance the script and re-run. Do NOT proceed. - -### Iteration 3: Gap analysis - -List EVERY claim in the Typst proof and whether it's tested: - -``` -CLAIM TESTED BY -"Universe has 2n elements" Section 4: overhead -"Complementarity forces consistency" Section 3: extraction -"Forward: NAE-sat -> valid splitting" Section 2: exhaustive -... -``` - -If any claim has no test, add one. If untestable, document WHY. - -## Step 5: Adversary Verification - -Dispatch a subagent that reads ONLY the Typst proof (not the constructor script) and independently implements + tests the reduction. - -**Adversary requirements:** -- Own `reduce()` function from scratch -- Own `extract_solution()` function -- Own `is_feasible_source()` and `is_feasible_target()` validators -- Exhaustive forward + backward for n <= 5 -- `hypothesis` property-based testing (>=2 strategies) -- Reproduce both Typst examples (YES and NO) -- >=5,000 total checks -- Must NOT import from the constructor script - -**Typed adversary focus** (include in prompt): -- **Identity reductions:** exhaustive enumeration n <= 6, edge-case configs (all-zero, all-one, alternating) -- **Algebraic reductions:** case boundary conditions (e.g., S = 2T exactly, S = 2T +/- 1), per-case extraction -- **Gadget reductions:** widget structure invariants, traversal patterns, interior vertex isolation - -### Cross-comparison - -After both scripts pass, compare `reduce()` outputs on shared instances. Both must produce structurally identical targets and agree on feasibility for all tested instances. - -### Verdict table - -| Constructor | Adversary | Cross-compare | Verdict | Action | -|-------------|-----------|---------------|---------|--------| -| Pass | Pass | Agree | **VERIFIED** | Done (or proceed to add-rule Step 2) | -| Pass | Pass | Disagree | **Suspect** | Investigate — may be isomorphic or latent bug | -| Pass | Fail | -- | **Adversary bug** | Fix adversary or clarify Typst spec | -| Fail | Pass | -- | **Constructor bug** | Fix constructor, re-run from Step 4 | -| Fail | Fail | -- | **Proof bug** | Re-examine Typst proof, return to Step 2 | - -## Step 6: Self-Review Checklist - -Every item must be YES. If any is NO, go back and fix. - -### Typst proof -- [ ] Construction with numbered steps, symbols defined before use -- [ ] Correctness with independent => and <= paragraphs -- [ ] Solution extraction section present -- [ ] Overhead table with formulas -- [ ] YES example (>=3 variables, fully worked) -- [ ] NO example (fully worked, explains WHY infeasible) -- [ ] Zero hand-waving language -- [ ] Zero scratch work - -### Type gate -- [ ] Concrete Rust `Value` types fully resolved with substitution evidence -- [ ] Different numeric domains either rejected or covered by an explicit full-domain range proof - -### Constructor Python -- [ ] 0 failures, >=5,000 total checks -- [ ] All 7 sections present and non-empty -- [ ] Exhaustive n <= 5 -- [ ] Extraction tested for every feasible instance -- [ ] Gap analysis: every Typst claim has a test - -### Adversary Python -- [ ] 0 failures, >=5,000 total checks -- [ ] Independent implementation (no imports from constructor) -- [ ] `hypothesis` PBT with >=2 strategies -- [ ] Reproduces both Typst examples - -### Cross-consistency -- [ ] Cross-comparison: 0 disagreements, 0 feasibility mismatches - -## Step 7: Report Verdict - -Report the final verdict to the user: - -``` -VERIFICATION RESULT: VERIFIED / FAILED - Source: - Target: - Constructor checks: - Adversary checks: - Cross-comparison: instances, 0 disagreements - Issue: # +VERIFICATION RESULT: VERIFIED / FAILED / INCOMPLETE + Source and target: + Mathematical claim and applicability domain:

+ Constructor coverage: + Independent coverage: + Cross-comparison: + Remaining gaps or counterexamples:
+ Backend integration, if run: ``` -If called as a subroutine of `/add-rule`, the verified Python `reduce()`, `extract_solution()`, and YES/NO instances remain in conversation context for use in the Rust implementation steps. No files are saved. - -If called standalone, the verdict is the final output. The user can inspect the proof and scripts interactively during the session. - -## Common Mistakes - -| Mistake | Consequence | -|---------|-------------| -| Proceeding past type gate with incompatible types | Wasted work — math may be correct but `ReduceTo` impl is impossible | -| Adversary imports from constructor script | Rejected — must be independent | -| No `hypothesis` PBT in adversary | Rejected | -| Section 1 (symbolic) empty | Rejected — "overhead is trivial" is not an excuse | -| Only YES example, no NO example | Rejected | -| n <= 3 or n <= 4 "because it's simple" | Rejected — minimum n <= 5 | -| No gap analysis | Rejected — perform before proceeding | -| Example has < 3 variables | Rejected — too degenerate | -| Either script has < 5,000 checks | Rejected — enhance testing | -| Extraction (Section 3) not tested | Rejected — most commonly skipped | -| Cross-comparison skipped | Rejected | -| Disagreements dismissed without investigation | Rejected | -| Saving artifacts to the repository | All files are ephemeral — use temp directory, nothing committed | +Use VERIFIED only when the proof and independent checks support the stated claim; +use FAILED for an established defect and INCOMPLETE for unresolved evidence. +When called by `/add-rule`, provide the checked construction, extraction, and +examples for the Rust implementation. Keep proof/scripts/results temporary; do +not commit generated verification artifacts. + +## Reduction lifecycle responsibilities + +Apply the canonical [executed lifecycle](../../../docs/src/design.md#executed-reduction-lifecycle). +State the rule's instance domain, qualifying-witness premise, source guarantee, +and infeasibility interpretation. Check every qualifying tied optimum in small +exhaustive cases where ties are relevant. A witness flag alone does not prove +complete solvability or that adjacent path premises compose. + +Construct each executed result once and share target, witness, value, and +completion state. Outcome interpretation uses the rule's mathematical relation; +ordinary extraction assumes its premises. Keep necessary dynamic/JSON conversion +and reachable representation failures, but no checked/unchecked extraction or +pure forwarding wrappers. Do not add `SolutionAggregate` bounds to models or +mathematical mappings; it belongs to brute-force witness selection. diff --git a/.config/nextest.toml b/.config/nextest.toml index 402f0ff76..a4e5c4b83 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -7,15 +7,7 @@ # whole job was SIGTERM-killed at the runner level) — nextest reports it as a # clean per-test timeout failure instead of stalling the job. # -# The large headroom (300s, not a tight ~10s) is deliberate. The subprocess -# example tests in tests/suites/examples.rs shell out to `cargo run --example -# … --features ilp-highs`: -# - In the Test job, CI pre-builds those examples (see ci.yml) so the -# subprocess reuses artifacts and each test runs in well under a second. -# - In the Code Coverage job, the subprocess inherits llvm-cov's -# `-C instrument-coverage` RUSTFLAGS, so it recompiles the examples -# *instrumented* (a non-instrumented pre-build would not match its -# fingerprint, so pre-building there is pointless). That instrumented -# recompile legitimately takes >120s, hence the 300s bound. +# The large headroom (300s, not a tight ~10s) is deliberate: this is a final +# safety bound for genuinely hung tests, not the expected runtime budget. [profile.default] slow-timeout = { period = "60s", terminate-after = 5 } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ef1a9fd8..aa3f64c0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,5 +191,5 @@ jobs: uses: codecov/codecov-action@v5 with: files: lcov.info - fail_ci_if_error: false # Don't fail CI if upload fails + fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} diff --git a/Cargo.toml b/Cargo.toml index 1fa1906fa..6f5d69279 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,10 +27,11 @@ serde = { version = "1.0", features = ["derive"] } # Persisted problem and reduction data must preserve every finite f64 on replay. serde_json = { version = "1.0", features = ["float_roundtrip"] } thiserror = "2.0" -num-bigint = "0.4" -num-rational = "0.4" +num-bigint = { version = "0.4", features = ["serde"] } +num-rational = { version = "0.4", features = ["serde"] } num-traits = "0.2" -good_lp = { version = "=1.14.2", default-features = false, features = ["highs"] } +sprs = { version = "0.11.5", default-features = false, features = ["serde"] } +highs = "=2.4.0" inventory = "0.3" ordered-float = "5.0" rand = "0.10" diff --git a/Makefile b/Makefile index 1cb637f32..1a3634480 100644 --- a/Makefile +++ b/Makefile @@ -163,10 +163,13 @@ paper: cargo run --features "$(TEST_FEATURES)" --example export_schemas typst compile --root . docs/paper/reductions.typ docs/paper/reductions.pdf -# Generate coverage report (requires: cargo install cargo-llvm-cov) +# Check changed-line coverage against the PR base, including uncommitted changes. +COVERAGE_BASE ?= origin/main +# Requires cargo-llvm-cov and uv. coverage: @command -v cargo-llvm-cov >/dev/null 2>&1 || { echo "Installing cargo-llvm-cov..."; cargo install cargo-llvm-cov; } - cargo llvm-cov --workspace --html --open + cargo llvm-cov --workspace --lcov --output-path target/coverage.lcov + uvx diff-cover target/coverage.lcov --compare-branch $(COVERAGE_BASE) --fail-under 95 --total-percent-float --format html:target/coverage-diff.html # Clean build artifacts clean: diff --git a/benches/solver_benchmarks.rs b/benches/solver_benchmarks.rs index 72dffb2e4..9a66bbe22 100644 --- a/benches/solver_benchmarks.rs +++ b/benches/solver_benchmarks.rs @@ -17,7 +17,8 @@ fn bench_independent_set(c: &mut Criterion) { for n in [4, 6, 8, 10].iter() { // Create a path graph with n vertices let edges: Vec<(usize, usize)> = (0..*n - 1).map(|i| (i, i + 1)).collect(); - let problem = MaximumIndependentSet::new(SimpleGraph::new(*n, edges), vec![1i64; *n]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(*n, edges).unwrap(), vec![1i64; *n]); let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { @@ -34,7 +35,7 @@ fn bench_vertex_covering(c: &mut Criterion) { for n in [4, 6, 8, 10].iter() { let edges: Vec<(usize, usize)> = (0..*n - 1).map(|i| (i, i + 1)).collect(); - let problem = MinimumVertexCover::new(SimpleGraph::new(*n, edges), vec![1i64; *n]); + let problem = MinimumVertexCover::new(SimpleGraph::new(*n, edges).unwrap(), vec![1i64; *n]); let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { @@ -52,7 +53,7 @@ fn bench_max_cut(c: &mut Criterion) { for n in [4, 6, 8, 10].iter() { let edges: Vec<(usize, usize)> = (0..*n - 1).map(|i| (i, i + 1)).collect(); let weights = vec![1i64; edges.len()]; - let problem = MaxCut::new(SimpleGraph::new(*n, edges), weights); + let problem = MaxCut::new(SimpleGraph::new(*n, edges).unwrap(), weights); let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { @@ -120,7 +121,7 @@ fn bench_set_covering(c: &mut Criterion) { let sets: Vec> = (0..*num_sets) .map(|i| vec![i, (i + 1) % *num_sets, (i + 2) % *num_sets]) .collect(); - let problem = MinimumSetCovering::::new(*num_sets, sets); + let problem = MinimumSetCovering::::new(*num_sets, sets).unwrap(); let solver = BruteForce::new(); group.bench_with_input( @@ -139,7 +140,7 @@ fn bench_coloring(c: &mut Criterion) { for n in [3, 4, 5, 6].iter() { let edges: Vec<(usize, usize)> = (0..*n - 1).map(|i| (i, i + 1)).collect(); - let problem = KColoring::::new(SimpleGraph::new(*n, edges)); + let problem = KColoring::::new(SimpleGraph::new(*n, edges).unwrap()); let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path_3colors", n), n, |b, _| { @@ -157,7 +158,7 @@ fn bench_matching(c: &mut Criterion) { for n in [4, 6, 8, 10].iter() { let edges: Vec<(usize, usize)> = (0..*n - 1).map(|i| (i, i + 1)).collect(); let weights = vec![1i64; edges.len()]; - let problem = MaximumMatching::new(SimpleGraph::new(*n, edges), weights); + let problem = MaximumMatching::new(SimpleGraph::new(*n, edges).unwrap(), weights); let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { @@ -178,7 +179,7 @@ fn bench_paintshop(c: &mut Criterion) { .flat_map(|i| vec![format!("car{}", i)]) .chain((0..*n).map(|i| format!("car{}", i))) .collect(); - let problem = PaintShop::from_strings(sequence); + let problem = PaintShop::from_strings(sequence).unwrap(); let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("sequential", n), n, |b, _| { @@ -197,7 +198,7 @@ fn bench_comparison(c: &mut Criterion) { // MaximumIndependentSet with 8 vertices let is_problem = MaximumIndependentSet::new( - SimpleGraph::new(8, vec![(0, 1), (2, 3), (4, 5), (6, 7)]), + SimpleGraph::new(8, vec![(0, 1), (2, 3), (4, 5), (6, 7)]).unwrap(), vec![1i64; 8], ); group.bench_function("MaximumIndependentSet", |b| { @@ -231,7 +232,7 @@ fn bench_comparison(c: &mut Criterion) { // MaxCut with 8 vertices let mc_problem = MaxCut::new( - SimpleGraph::new(8, vec![(0, 1), (2, 3), (4, 5), (6, 7)]), + SimpleGraph::new(8, vec![(0, 1), (2, 3), (4, 5), (6, 7)]).unwrap(), vec![1, 1, 1, 1], ); group.bench_function("MaxCut", |b| { diff --git a/codecov.yml b/codecov.yml index 27c6f0cd2..7091d9e6f 100644 --- a/codecov.yml +++ b/codecov.yml @@ -10,11 +10,11 @@ coverage: project: default: target: 95% - threshold: 2% + threshold: 0% patch: default: target: 95% - threshold: 2% + threshold: 0% # Exclude proc-macro crate from coverage since it runs at compile time # and traditional runtime coverage tools cannot measure it. diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index ee531965e..ad8527b32 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -369,7 +369,6 @@ "ShortestCommonSuperstring": [Shortest Common Superstring], "StaffScheduling": [Staff Scheduling], "SteinerTree": [Steiner Tree], - "SteinerTreeInGraphs": [Steiner Tree in Graphs], "MinimumAxiomSet": [Minimum Axiom Set], "MinimumExternalMacroDataCompression": [Minimum External Macro Data Compression], "MinimumInternalMacroDataCompression": [Minimum Internal Macro Data Compression], @@ -1342,7 +1341,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| } #{ - let x = load-model-example("DecisionMinimumVertexCover") + let x = load-model-example("DecisionMinimumVertexCover", variant: (graph: "SimpleGraph", weight: "i64")) let inner = x.instance.inner let nv = graph-num-vertices(x.instance) let ne = graph-num-edges(x.instance) @@ -3219,11 +3218,13 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let steiner-verts = tree-verts.filter(v => not terminals.contains(v)) [ #problem-def("SteinerTree")[ - Given an undirected graph $G = (V, E)$ with edge weights $w: E -> RR_(>= 0)$ and a set of terminal vertices $T subset.eq V$ with $|T| >= 2$, find a tree $S = (V_S, E_S)$ in $G$ such that $T subset.eq V_S$, minimizing $sum_(e in E_S) w(e)$. Vertices in $V_S backslash T$ are called _Steiner vertices_. + Given an undirected graph $G = (V, E)$ with edge weights $w: E -> ZZ$ and a set of terminal vertices $T subset.eq V$ with $|T| >= 1$, find a tree $S = (V_S, E_S)$ in $G$ such that $T subset.eq V_S$, minimizing $sum_(e in E_S) w(e)$. Vertices in $V_S backslash T$ are called _Steiner vertices_. ][ One of Karp's 21 NP-complete problems @karp1972, foundational in network design with applications in telecommunications backbone routing, VLSI chip interconnect, pipeline planning, and phylogenetic tree construction. When $T = V$, the problem reduces to the minimum spanning tree (polynomial). The NP-hardness arises from choosing which Steiner vertices to include. - The best known exact algorithm runs in $O^*(3^(|T|) dot n + 2^(|T|) dot n^2)$ time via Dreyfus--Wagner dynamic programming over terminal subsets @dreyfuswagner1971. Byrka _et al._ achieved a $ln(4) + epsilon approx 1.39$-approximation @byrka2013; the classic 2-approximation uses the minimum spanning tree of the terminal distance graph. + For nonnegative weights, Dreyfus--Wagner runs in $O^*(3^(|T|) dot n + 2^(|T|) dot n^2)$ time using dynamic programming over terminal subsets @dreyfuswagner1971. Byrka _et al._ achieved a $ln(4) + epsilon approx 1.39$-approximation @byrka2013; the classic 2-approximation uses the minimum spanning tree of the terminal distance graph. + + For signed weights, enumerating the $2^(n-|T|)$ nonterminal subsets and computing a minimum spanning tree on each induced graph gives an $O(2^(n-|T|) n^2)$ exact algorithm. Selected edges must still form a single acyclic tree; disconnected negative edges and cycles are invalid. With one terminal, the zero-edge tree at that terminal is feasible, but a tree containing negative edges can have lower cost. // Find the unique direct terminal-terminal edge (both endpoints in T, not in the optimal tree) #let terminal-set = terminals @@ -3809,74 +3810,6 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] } -#{ - let x = load-model-example("SteinerTreeInGraphs") - let nv = graph-num-vertices(x.instance) - let edges = x.instance.graph.edges - let ne = edges.len() - let terminals = x.instance.terminals - let weights = x.instance.edge_weights - let sol = (config: x.optimal_config, metric: x.optimal_value) - let opt-weight = metric-value(sol.metric) - // Derive tree edges from optimal config - let tree-edge-indices = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) - let tree-edges = tree-edge-indices.map(i => edges.at(i)) - // Steiner vertices: non-terminal vertices that appear in tree edges - let steiner-verts = range(nv).filter(v => not terminals.contains(v) and tree-edges.any(e => e.at(0) == v or e.at(1) == v)) - [ - #problem-def("SteinerTreeInGraphs")[ - Given an undirected graph $G = (V, E)$ with edge weights $w: E -> RR_(>= 0)$ and a set of terminal vertices $R subset.eq V$, find a subtree $T$ of $G$ that spans all terminals in $R$ and minimizes the total edge weight $sum_(e in T) w(e)$. - ][ - A classical NP-complete problem from Karp's list (as "Steiner Tree in Graphs," Garey & Johnson ND12) @karp1972. Central to network design, VLSI layout, and phylogenetic reconstruction. The problem generalizes minimum spanning tree (where $R = V$) and shortest path (where $|R| = 2$). The Dreyfus--Wagner dynamic programming algorithm @dreyfuswagner1971 solves it in $O(3^k dot n + 2^k dot n^2 + n^3)$ time, where $k = |R|$ and $n = |V|$. Bjorklund et al. @bjorklund2007 achieved $O^*(2^k)$ using subset convolution over the Mobius algebra, and Nederlof @nederlof2009 gave an $O^*(2^k)$ polynomial-space algorithm. - - *Example.* Consider a graph $G$ with $n = #nv$ vertices and $|E| = #ne$ edges. The terminals are $R = {#terminals.map(i => $v_#i$).join(", ")}$ (blue). The optimal Steiner tree uses Steiner vertex #steiner-verts.map(i => $v_#i$).join(", ") (gray, dashed border) and edges #tree-edges.map(e => [$\{v_#(e.at(0)), v_#(e.at(1))\}$]).join(", ") with total weight #tree-edge-indices.map(i => str(weights.at(i))).join(" + ") $= #opt-weight$. - - #pred-commands( - "pred create --example SteinerTreeInGraphs -o steiner-tree-in-graphs.json", - "pred solve steiner-tree-in-graphs.json", - "pred evaluate steiner-tree-in-graphs.json --config " + cli-config(x.optimal_config), - ) - - #figure({ - // Graph: 6 vertices arranged in two rows (layout positions) - let verts = ((0, 1), (1.5, 1), (3, 1), (1.5, -0.5), (3, -0.5), (4.5, 0.25)) - canvas(length: 1cm, { - import draw: * - // Edge (0,2) idx=1 would otherwise pass straight through the collinear - // vertex $v_1$ at $(1.5, 1)$, so route it as a quadratic Bezier arc above. - let arc-ctrl = ("1": (1.5, 1.85)) - for (idx, (u, v)) in edges.enumerate() { - let on-tree = tree-edges.any(t => (t.at(0) == u and t.at(1) == v) or (t.at(0) == v and t.at(1) == u)) - let stk = if on-tree { 2pt + graph-colors.at(0) } else { 1pt + luma(200) } - let key = str(idx) - if key in arc-ctrl { - let c = arc-ctrl.at(key) - bezier(verts.at(u), verts.at(v), c, stroke: stk) - let mx = 0.25 * verts.at(u).at(0) + 0.5 * c.at(0) + 0.25 * verts.at(v).at(0) - let my = 0.25 * verts.at(u).at(1) + 0.5 * c.at(1) + 0.25 * verts.at(v).at(1) - draw.content((mx, my + 0.18), text(7pt, fill: luma(80))[#weights.at(idx)]) - } else { - g-edge(verts.at(u), verts.at(v), stroke: stk) - let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2 - let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2 - draw.content((mx, my), text(7pt, fill: luma(80))[#weights.at(idx)]) - } - } - for (k, pos) in verts.enumerate() { - let is-terminal = terminals.contains(k) - let is-steiner = steiner-verts.contains(k) - g-node(pos, name: "v" + str(k), - fill: if is-terminal { graph-colors.at(0) } else if is-steiner { luma(220) } else { white }, - stroke: if is-steiner { (dash: "dashed", paint: graph-colors.at(0)) } else { 1pt + black }, - label: if is-terminal { text(fill: white)[$v_#k$] } else { [$v_#k$] }) - } - }) - }, - caption: [Steiner Tree: terminals $R = {#terminals.map(i => $v_#i$).join(", ")}$ (blue), Steiner vertex #steiner-verts.map(i => $v_#i$).join(", ") (dashed). Optimal tree (blue edges) has weight #opt-weight.], - ) - ] - ] -} #{ let x = load-model-example("MinimumSumMulticenter") @@ -4945,10 +4878,21 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] } +// Expand small sparse QUBO examples only for typesetting their matrices. +#let qubo-matrix(instance) = { + let m = instance.matrix + range(m.nrows).map(i => { + let row = range(m.ncols).map(_ => 0) + for k in range(m.indptr.at(i), m.indptr.at(i + 1)) { + row.at(m.indices.at(k)) = m.data.at(k) + } + row + }) +} #{ let x = load-model-example("QUBO") - let n = x.instance.num_vars - let Q = x.instance.matrix + let n = x.instance.matrix.nrows + let Q = qubo-matrix(x.instance) let sol = (config: x.optimal_config, metric: x.optimal_value) let xstar = sol.config let fstar = metric-value(sol.metric) @@ -5436,21 +5380,20 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let basis = x.instance.basis let target = x.instance.target let sol = (config: x.optimal_config, metric: x.optimal_value) - let dist = metric-value(sol.metric) let coords = sol.config // Compute B*x: sum over j of coords[j] * basis[j] let dim = basis.at(0).len() let bx = range(dim).map(d => coords.enumerate().fold(0.0, (acc, (j, c)) => acc + c * basis.at(j).at(d))) // Format basis vectors let fmt-vec(v) = $paren.l #v.map(e => str(e)).join(", ") paren.r^top$ - let dist-rounded = calc.round(dist, digits: 3) + let distance-squared = range(dim).fold(0, (total, d) => total + calc.pow(bx.at(d) - target.at(d), 2)) [ #problem-def("ClosestVectorProblem")[ - Given a full-column-rank integer lattice basis $bold(B) in ZZ^(m times n)$, whose columns span $cal(L)(bold(B)) = {bold(B) bold(x) : bold(x) in ZZ^n}$, and target $bold(t) in RR^m$, find $bold(x) in ZZ^n$ minimizing $norm(bold(B) bold(x) - bold(t))_2$. + Given a full-column-rank integer lattice basis $bold(B) in ZZ^(m times n)$, whose columns span $cal(L)(bold(B)) = {bold(B) bold(x) : bold(x) in ZZ^n}$, and target $bold(t) in RR^m$, find $bold(x) in ZZ^n$ minimizing $norm(bold(B) bold(x) - bold(t))_2^2$. ][ - The Closest Vector Problem is a fundamental lattice problem @micciancio2002 and is NP-hard @vanemde1981. The implementation provides an integer-target variant for exact reduction data and a finite-`f64` target variant for real input; both keep the lattice basis integral and place no bounds on $bold(x)$. Its reference solver uses exact rational Gram--Schmidt projections and sphere-enumeration bounds following the recursive enumeration structure of Fincke and Pohst @fincke1985. Finite `f64` targets are interpreted as their exact binary rational values. The solver is intended for small instances. Kannan's enumeration algorithm @kannan1987 solves CVP in $n^(O(n))$ time; Micciancio and Voulgaris @micciancio2010 improved this to deterministic $O^*(4^n)$, and Aggarwal, Dadush, and Stephens-Davidowitz @aggarwal2015 achieved randomized $O^*(2^n)$. + The Closest Vector Problem is a fundamental lattice problem @micciancio2002 and is NP-hard @vanemde1981. The implementation provides an integer-target variant for exact reduction data and a finite-`f64` target variant for real input; both keep the lattice basis integral and place no bounds on $bold(x)$. Its reference solver uses exact rational Gram--Schmidt projections and sphere-enumeration bounds following the recursive enumeration structure of Fincke and Pohst @fincke1985. Model evaluation returns the squared distance as an exact rational, preserving the minimizers of Euclidean distance. Finite `f64` targets are interpreted as their exact binary rational values. The solver is intended for small instances. Kannan's enumeration algorithm @kannan1987 solves CVP in $n^(O(n))$ time; Micciancio and Voulgaris @micciancio2010 improved this to deterministic $O^*(4^n)$, and Aggarwal, Dadush, and Stephens-Davidowitz @aggarwal2015 achieved randomized $O^*(2^n)$. - *Example.* Consider the 2D lattice with basis #range(basis.len()).map(j => $bold(b)_#(j + 1) = #fmt-vec(basis.at(j))$).join(", ") and target $bold(t) = #fmt-vec(target)$. The point $bold(B)(#coords.map(c => str(c)).join(","))^top = (#bx.map(v => str(int(v))).join(", "))^top$ equals the target, so it is a closest lattice point with distance #dist-rounded. + *Example.* Consider the 2D lattice with basis #range(basis.len()).map(j => $bold(b)_#(j + 1) = #fmt-vec(basis.at(j))$).join(", ") and target $bold(t) = #fmt-vec(target)$. The point $bold(B)(#coords.map(c => str(c)).join(","))^top = (#bx.map(v => str(int(v))).join(", "))^top$ equals the target, so it is a closest lattice point with squared distance #distance-squared. #pred-commands( "pred create --example ClosestVectorProblem -o closest-vector-problem.json", @@ -5485,7 +5428,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| content((rel: (-0.3, 0), to: "b2.mid"), text(7pt)[$bold(b)_2$]) content((rel: (0.45, 0.3), to: "p" + str(coords.at(0)) + str(coords.at(1))), text(7pt)[$bold(B)(#coords.map(c => str(c)).join(","))^top$]) }), - caption: [2D lattice with basis #range(basis.len()).map(j => $bold(b)_#(j + 1) = #fmt-vec(basis.at(j))$).join(", "). Target $bold(t) = #fmt-vec(target)$ (red) and closest lattice point $bold(B)(#coords.map(c => str(c)).join(","))^top = (#bx.map(v => str(int(v))).join(", "))^top$ (blue). Distance $approx #dist-rounded$.], + caption: [2D lattice with basis #range(basis.len()).map(j => $bold(b)_#(j + 1) = #fmt-vec(basis.at(j))$).join(", "). Target $bold(t) = #fmt-vec(target)$ (red) and closest lattice point $bold(B)(#coords.map(c => str(c)).join(","))^top = (#bx.map(v => str(int(v))).join(", "))^top$ (blue). Squared distance $#distance-squared$.], ) ] ] @@ -7604,7 +7547,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| $ min_(c, p_1, dots, p_n) max_(1 lt.eq i lt.eq n) d_H (c, s_i [p_i .. p_i + ell)), $ where $d_H$ is the Hamming distance and $s_i [p_i .. p_i + ell)$ is the length-$ell$ substring of $s_i$ starting at position $p_i$. ][ - Introduced by #cite(, form: "prose"), who showed that the decision version is NP-complete (even over the binary alphabet) and gave the first polynomial-time approximation scheme. Closest Substring strictly generalizes Closest String: the special case $ell = |s_i|$ for all $i$ forces a unique window in each string and recovers Closest String. The registered exact baseline enumerates every center in $Sigma^ell$ together with every tuple of window starts, giving $O(q^ell dot product_i (|s_i| - ell + 1))$ configurations. + Introduced by #cite(, form: "prose"), who showed that the decision version is NP-complete (even over the binary alphabet) and gave the first polynomial-time approximation scheme. Closest Substring strictly generalizes Closest String: the special case $ell = |s_i|$ for all $i$ forces a unique window in each string and recovers Closest String. The registered exact baseline enumerates every center in $Sigma^ell$ together with every tuple of window starts, giving $O(q^ell dot product_i (|s_i| - ell + 1))$ configurations. Writing $W = sum_i (|s_i| - ell + 1)$, AM–GM bounds this count by $q^ell (W/n)^n$; the registered complexity uses this bound without storing the product as an instance parameter. *Example.* Let $Sigma = {0, 1}$ ($q = #alphabet-size$), $ell = #ell$, and consider the $n = #n$ binary strings $s_1 = #fmt-str(strings.at(0))$, $s_2 = #fmt-str(strings.at(1))$, $s_3 = #fmt-str(strings.at(2))$. @@ -8097,7 +8040,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #problem-def("KthLargestMTuple")[ Given $m$ finite sets $X_1, dots, X_m$ of positive integers, a bound $B in ZZ^+$, and a threshold $K in ZZ^+$, count the number of distinct $m$-tuples $(x_1, dots, x_m) in X_1 times dots.c times X_m$ satisfying $sum_(i=1)^m x_i >= B$. The answer is _yes_ iff this count is at least $K$. ][ - The $K$th Largest $m$-Tuple problem is MP10 in Garey and Johnson's appendix @garey1979. It is _not known to be in NP_, because a "yes" certificate may need to exhibit $K$ qualifying tuples and $K$ can be exponentially large. The problem is PP-complete under polynomial-time Turing reductions @haase2016, though the special case $m = 2$, $K = 1$ is NP-complete via reduction from Subset Sum. In the general case, the only known exact approach is brute-force enumeration of all $product_(i=1)^m |X_i|$ tuples, so the registered catalog complexity is `total_tuples * num_sets`#footnote[No algorithm improving on brute-force is known for the general $K$th Largest $m$-Tuple problem.]. + The $K$th Largest $m$-Tuple problem is MP10 in Garey and Johnson's appendix @garey1979. It is _not known to be in NP_, because a "yes" certificate may need to exhibit $K$ qualifying tuples and $K$ can be exponentially large. The problem is PP-complete under polynomial-time Turing reductions @haase2016, though the special case $m = 2$, $K = 1$ is NP-complete via reduction from Subset Sum. In the general case, the only known exact approach is brute-force enumeration of all $product_(i=1)^m |X_i|$ tuples, so AM–GM gives the registered catalog bound `(num_elements / num_sets)^num_sets * num_sets`#footnote[No algorithm improving on brute-force is known for the general $K$th Largest $m$-Tuple problem.]. *Example.* Let $m = #m$, $B = #bound$, and $K = #k$ with sets #sets.enumerate().map(((i, s)) => [$X_#(i+1) = {#fmt-values(s)}$]).join([, ]). The Cartesian product has $#total$ tuples. Exactly #k tuples have sum at least #bound, so the answer is _yes_ (count $= K$). The evaluator enumerates the Cartesian product internally and stops once it has found $K$ qualifying tuples. @@ -11978,7 +11921,7 @@ the displayed rule, extracted from the corresponding `pred path` entry. let basis = cvp_qubo.source.instance.basis let target = cvp_qubo.source.instance.target let coords = cvp_qubo_sol.source_config - let matrix = cvp_qubo.target.instance.matrix + let matrix = qubo-matrix(cvp_qubo.target.instance) let bits = cvp_qubo_sol.target_config let lower = (-23, -14) let anchor = range(target.len()).map(d => lower.enumerate().fold(0.0, (acc, (i, x)) => acc + x * basis.at(i).at(d))) @@ -12005,7 +11948,7 @@ the displayed rule, extracted from the corresponding `pred path` entry. *Step 2 -- Derive a safe box.* Here $A=((2,1),(0,2))$, $norm(bold(t))_1=5$, and the selected-row bounds are $bold(C)=(8,7)$. Since $op("adj")(A)=((2,-1),(0,2))$, the reduction obtains $M_1=23$ and $M_2=14$. - *Step 3 -- Encode and expand.* The exact-range weights are $(1,2,4,8,16,15)$ for $x_1+23 in [0,46]$ and $(1,2,4,8,13)$ for $x_2+14 in [0,28]$, giving #cvp_qubo.target.instance.num_vars variables. With $G=B^top B=((4,2),(2,5))$ and $h=B^top bold(t)=(6,7)^top$, representative coefficients are $Q_(0,0)=#matrix.at(0).at(0)$, $Q_(0,1)=#matrix.at(0).at(1)$, $Q_(0,6)=#matrix.at(0).at(6)$, and $Q_(6,6)=#matrix.at(6).at(6)$. + *Step 3 -- Encode and expand.* The exact-range weights are $(1,2,4,8,16,15)$ for $x_1+23 in [0,46]$ and $(1,2,4,8,13)$ for $x_2+14 in [0,28]$, giving #cvp_qubo.target.instance.matrix.nrows variables. With $G=B^top B=((4,2),(2,5))$ and $h=B^top bold(t)=(6,7)^top$, representative coefficients are $Q_(0,0)=#matrix.at(0).at(0)$, $Q_(0,1)=#matrix.at(0).at(1)$, $Q_(0,6)=#matrix.at(0).at(6)$, and $Q_(6,6)=#matrix.at(6).at(6)$. *Step 4 -- Verify a solution.* The fixture stores $bold(z)=(#fmt-values(bits))$, which decodes to $bold(x)=(#fmt-values(coords))$. The QUBO value is #rounded-qubo; adding the dropped constant #rounded-constant gives squared CVP distance #rounded-distance-sq, so $B bold(x)=bold(t)$ #sym.checkmark. @@ -12252,7 +12195,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m together with target $ bold(t) = (#fmt-values(ss-cvp-target-vec))^top $ in the standard CVP model, with no coefficient bounds. - *Step 3 -- Verify the canonical witness.* The fixture stores coefficients $(#fmt-values(ss-cvp-x))$. Its first four entries select sizes $3$ and $8$, and the final three are carry coefficients. The first coordinate block has residual $(1,0,0,1)$, the second has $(0,-1,-1,0)$, and all bit-equation residuals are zero. Thus the Euclidean distance is $sqrt(4) = 2$. + *Step 3 -- Verify the canonical witness.* The fixture stores coefficients $(#fmt-values(ss-cvp-x))$. Its first four entries select sizes $3$ and $8$, and the final three are carry coefficients. The first coordinate block has residual $(1,0,0,1)$, the second has $(0,-1,-1,0)$, and all bit-equation residuals are zero. Thus the squared-distance objective is $4$. *Witness semantics.* The example DB stores one canonical minimizer. This source instance also has another satisfying subset, $(1, 1, 1, 0)$, so the reduction has multiple optimal CVP witnesses even though only one is serialized. ], @@ -12261,15 +12204,15 @@ where $P$ is a penalty weight large enough that any constraint violation costs m ][ _Construction._ Let $n$ be the number of items, and let $b >= 1$ be the maximum bit length of their nonnegative sizes and target $T$. Write $s_(i,j), t_j in {0,1}$ for bit $j$ of size $s_i$ and target $T$. Introduce integer coefficients $x_0, dots, x_(n-1)$ and carries $c_1, dots, c_(b-1)$, with fixed boundary values $c_0=c_b=0$. The displacement vector consists of $x_i$ for all items, then $x_i-1$ for all items, then residuals $ r_j = sum_(i=0)^(n-1) s_(i,j) x_i + c_j - 2 c_(j+1) - t_j $ - in descending bit order. These linear expressions define the integer basis columns and a target containing only zeros and ones. Carry columns are also ordered by descending bit index. The first $n$ coordinate rows form an identity on item columns; the remaining carry block has unit pivots in this order. Consequently the full-column-rank check has no exponentially growing pivots. + in descending bit order. These linear expressions define the integer basis columns and a target containing only zeros and ones. Carry columns are also ordered by descending bit index. The first $n$ coordinate rows form an identity on item columns; the remaining carry block has unit pivots in this order. This triangular block together with the item identity proves full column rank. _Correctness._ Every integer vector satisfies $ norm(bold(B) bold(z)-bold(t))_2^2 = sum_i (x_i^2 + (x_i-1)^2) + sum_j r_j^2 >= n. $ - ($arrow.r.double$) For a binary subset summing to $T$, ordinary integer addition gives carries $0 <= c_j <= n$ satisfying all bit equations and both boundaries. Its squared distance equals $n$. ($arrow.l.double$) Squared distance at most $n$ forces each $x_i in {0,1}$ and each $r_j=0$. Multiplying the bit equations by $2^j$ and summing cancels the internal carries, yielding $sum_i s_i x_i=T$. Thus the optimum is $sqrt(n)$ exactly for YES instances. Empty item lists and target zero use the same construction. + ($arrow.r.double$) For a binary subset summing to $T$, ordinary integer addition gives carries $0 <= c_j <= n$ satisfying all bit equations and both boundaries. Its squared distance equals $n$. ($arrow.l.double$) Squared distance at most $n$ forces each $x_i in {0,1}$ and each $r_j=0$. Multiplying the bit equations by $2^j$ and summing cancels the internal carries, yielding $sum_i s_i x_i=T$. Thus the squared-distance optimum is $n$ exactly for YES instances. Empty item lists and target zero use the same construction. - _Solution extraction._ Validate the target configuration once and require a finite distance exactly $sqrt(n)$ through the formal aggregate certificate. Return the first $n$ coefficients as Boolean selections, accepting one as true; the remaining coefficients are the specified carries. A larger optimal distance proves NO and provides no source witness. + _Solution extraction._ Validate the target configuration once and require squared distance exactly $n$ through the formal aggregate certificate. Return the first $n$ coefficients as Boolean selections, accepting one as true; the remaining coefficients are the specified carries. A larger optimal distance proves NO and provides no source witness. - _Representation._ The target has $2n+b$ coordinates and $n+b-1$ basis columns. Since bit length is not a registered Subset Sum parameter, the symbolic relations are marked unavailable with that reason. Dimensions and the total dense basis byte count are checked before allocation. On a 64-bit platform this bounds $n < 2^30$; the threshold and the unit squared-distance gap remain distinguishable in the target's floating-point evaluation. The paired coordinates and boundary carry equations also ensure every threshold witness has exactly evaluated small integer residuals. The solver uses exact rational sphere-enumeration bounds; runtime limitations are separate from the mathematical equivalence. + _Representation._ The target has $2n+b$ coordinates and $n+b-1$ basis columns. Since bit length is not a registered Subset Sum parameter, the symbolic relations are marked unavailable with that reason. Dimensions and the total dense basis byte count are checked before allocation. The model evaluates squared distances in exact rational arithmetic and the solver uses exact rational sphere-enumeration bounds; runtime limitations are separate from the mathematical equivalence. ] ] } @@ -12441,7 +12384,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m #let ks_qubo = load-example("Knapsack", "QUBO") #let ks_qubo_sol = ks_qubo.solutions.at(0) #let ks_qubo_num_items = ks_qubo.source.instance.weights.len() -#let ks_qubo_num_slack = ks_qubo.target.instance.num_vars - ks_qubo_num_items +#let ks_qubo_num_slack = ks_qubo.target.instance.matrix.nrows - ks_qubo_num_items #let ks_qubo_penalty = 1 + ks_qubo.source.instance.values.fold(0, (a, b) => a + b) #let ks_qubo_selected = ks_qubo_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #let ks_qubo_sel_weight = ks_qubo_selected.fold(0, (a, i) => a + ks_qubo.source.instance.weights.at(i)) @@ -12460,7 +12403,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m *Step 2 -- Introduce slack variables.* The inequality $sum_i w_i x_i lt.eq C$ becomes an equality by adding $B = #ks_qubo_num_slack$ binary slack bits that encode unused capacity: $ #ks_qubo.source.instance.weights.enumerate().map(((i, w)) => $#w x_#i$).join($+$) + #range(ks_qubo_num_slack).map(j => $#calc.pow(2, j) s_#j$).join($+$) = #ks_qubo.source.instance.capacity $ - This gives $n + B = #ks_qubo_num_items + #ks_qubo_num_slack = #ks_qubo.target.instance.num_vars$ QUBO variables. + This gives $n + B = #ks_qubo_num_items + #ks_qubo_num_slack = #ks_qubo.target.instance.matrix.nrows$ QUBO variables. *Step 3 -- Add the penalty objective.* With penalty $P = 1 + sum_i v_i = #ks_qubo_penalty$, the QUBO minimizes $ H = -(#ks_qubo.source.instance.values.enumerate().map(((i, v)) => $#v x_#i$).join($+$)) + #ks_qubo_penalty (#ks_qubo.source.instance.weights.enumerate().map(((i, w)) => $#w x_#i$).join($+$) + #range(ks_qubo_num_slack).map(j => $#calc.pow(2, j) s_#j$).join($+$) - #ks_qubo.source.instance.capacity)^2 $ @@ -12501,7 +12444,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m *Step 2 -- One-hot variables.* Introduce one binary selector per sampled orientation: $ underbrace(y_(1,0) y_(1,1), "link 1") #h(6pt) underbrace(y_(2,0) y_(2,1), "link 2") $ - The QUBO therefore has $2 + 2 = #mdpik_qubo.target.instance.num_vars$ variables. + The QUBO therefore has $2 + 2 = #mdpik_qubo.target.instance.matrix.nrows$ variables. *Step 3 -- Quadratic energy.* The geometric coefficients are $c = (2, 0, 1, 0)$ for the $x$-coordinate and $s = (0, 2, 0, 1)$ for the $y$-coordinate, so the position term is $ (2 y_(1,0) + y_(2,0) - 2)^2 + (2 y_(1,1) + y_(2,1) - 1)^2. $ @@ -12515,18 +12458,18 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Construction._ For each link $j in {1, dots, n}$ and sample index $a in {0, dots, m_j - 1}$, introduce a binary variable $y_(j,a) in {0,1}$ with the intended meaning "$y_(j,a) = 1$ iff link $j$ chooses orientation $phi_(j,a)$." Define $ c_(j,a) = l_j cos phi_(j,a), quad s_(j,a) = l_j sin phi_(j,a). $ Let - $ P = 1 + (sum_(j,a) |c_(j,a)| + |g_x|)^2 + (sum_(j,a) |s_(j,a)| + |g_y|)^2. $ + $ D = (sum_(j,a) |c_(j,a)| + |g_x|)^2 + (sum_(j,a) |s_(j,a)| + |g_y|)^2, quad P = 2(1 + D). $ The QUBO objective is the sum of three terms: $ H = underbrace((sum_(j,a) c_(j,a) y_(j,a) - g_x)^2 + (sum_(j,a) s_(j,a) y_(j,a) - g_y)^2)_"position error" + underbrace(P sum_(j=1)^n (sum_(a=0)^(m_j - 1) y_(j,a) - 1)^2)_"one-hot" + underbrace(P sum_(j=2)^n sum_((a,b) in.not A_j) y_(j-1,a) y_(j,b))_"forbidden pairs". $ - Expanding with $y_(j,a)^2 = y_(j,a)$ gives the upper-triangular QUBO matrix. As usual, the additive constant $g_x^2 + g_y^2$ is dropped. + Expanding with $y_(j,a)^2 = y_(j,a)$ gives the upper-triangular QUBO matrix. The implementation drops the full additive constant $C = g_x^2 + g_y^2 + n P$, including the one-hot constants. - _Correctness._ ($arrow.r.double$) Any feasible inverse-kinematics configuration $a_1, dots, a_n$ maps to the one-hot assignment with $y_(j,a_j) = 1$ and all other selectors $0$. Every one-hot penalty vanishes, every consecutive pair lies in the relevant admissible set, and the remaining QUBO objective equals the squared end-effector distance up to the dropped additive constant. ($arrow.l.double$) If some link is not one-hot, then $(sum_a y_(j,a) - 1)^2 >= 1$, so the assignment pays at least $P$. If every link is one-hot but some consecutive pair is forbidden, then exactly one forbidden-pair monomial is active at that junction, again contributing at least $P$. By definition of $P$, every decoded source configuration has squared distance at most $P - 1$, while the dropped-constant geometric term is bounded below by $-(g_x^2 + g_y^2)$. Therefore every violating assignment has strictly larger energy than every feasible source assignment. Among the penalty-zero assignments, minimizing $H$ is exactly minimizing the source squared distance. + _Correctness._ ($arrow.r.double$) Any feasible inverse-kinematics configuration $a_1, dots, a_n$ maps to the one-hot assignment with $y_(j,a_j) = 1$ and all other selectors $0$. Every one-hot penalty vanishes, every consecutive pair lies in the relevant admissible set, and the remaining QUBO objective equals the squared end-effector distance up to the dropped additive constant. ($arrow.l.double$) If some link is not one-hot, then $(sum_a y_(j,a) - 1)^2 >= 1$, so the assignment pays at least $P$. If every link is one-hot but some consecutive pair is forbidden, then exactly one forbidden-pair monomial is active at that junction, again contributing at least $P$. Every feasible assignment has $H <= D$, whereas every violating assignment has $H >= P$. Thus a feasible source has only qualifying optima; if the source is infeasible, every target assignment has $H >= P$. Among the penalty-zero assignments, minimizing $H$ is exactly minimizing the source squared distance. - _Solution extraction._ For each link block $j$, read the unique active selector $y_(j,a) = 1$ and output its sample index $a$. If the decoded index vector violates an admissible-pair constraint, the source evaluator rejects it with `Min(None)`. + _Value recovery and extraction._ For target optimum $E$, compare $E$ to $3(1+D)/2 - C$, which lies strictly between the feasible and infeasible energy ranges. An optimum above this separator yields `Min(None)` for the source. Otherwise the source optimum is $E+C$, and each block has a unique active selector whose sample index is the source witness. Value recovery precedes extraction; extraction does not recheck one-hot or pair feasibility. Floating-point results follow the numerical contract; the scaled penalty leaves a gap proportional to the coefficient scale, and nonfinite construction arithmetic is an error. ] #let mwc_qubo = load-example("MinimumMultiwayCut", "QUBO") @@ -12564,23 +12507,22 @@ where $P$ is a penalty weight large enough that any constraint violation costs m *Step 6 -- Verify a solution.* The QUBO ground state $bold(x) = (#fmt-values(mwc_qubo_sol.target_config))$ decodes to the partition: vertex 0 in component 0, vertices 1--3 in component 1, vertex 4 in component 2. Cut edges: $\{#mwc_qubo_cut_indices.map(i => "(" + str(mwc_qubo_edges.at(i).at(0)) + "," + str(mwc_qubo_edges.at(i).at(1)) + ")").join(", ")\}$ with total weight #mwc_qubo_cut_indices.map(i => str(mwc_qubo_weights.at(i))).join(" + ") $= #mwc_qubo_cut_cost$ #sym.checkmark. ], )[ - The multiway cut problem requires a partition of vertices into $k$ components — one per terminal — minimizing the total weight of edges crossing components. The penalty method (@sec:penalty-method) encodes two constraints as QUBO penalties: (1) each vertex belongs to exactly one component (one-hot), and (2) each terminal is pinned to its own component. The cut-cost Hamiltonian counts edge weight across distinct components. Reference: @Heidari2022. + A multiway cut deletes edges to separate every terminal pair. For signed weights, every negative edge is deleted first; the remaining nonnegative problem is represented by a terminal-labelled partition. The penalty method (@sec:penalty-method) enforces one label per vertex and pins each terminal to its own label. Reference for the partition encoding: @Heidari2022. ][ - _Construction._ Given $G = (V, E)$ with $n = |V|$, edge weights $w: E -> RR_(>0)$, and $k$ terminals $T = {t_0, ..., t_(k-1)}$. Introduce $n k$ binary variables $x_(u,t) in {0,1}$ (indexed by $u dot k + t$), where $x_(u,t) = 1$ means vertex $u$ is in terminal $t$'s component. Let $alpha = 1 + sum_(e in E) w(e)$. + _Construction._ Given $G = (V, E)$ with integer weights $w: E -> ZZ$ and $k >= 2$ distinct terminals, let $w^+(e) = max(w(e), 0)$ and $C_- = sum_(e: w(e) < 0) w(e)$. Introduce $n k$ binary variables $x_(u,t)$, where label $t$ indicates the terminal group of vertex $u$. Let $alpha = 1 + sum_(e in E) w^+(e)$. - The QUBO Hamiltonian is $H = H_A + H_B$ where: + The Hamiltonian is $H = H_A + H_B$, with $ H_A = alpha (sum_(u in V) (1 - sum_(t=0)^(k-1) x_(u,t))^2 + sum_(i=0)^(k-1) sum_(s != i) x_(t_i, s)) $ - The first term is a _one-hot constraint_ ensuring each vertex is assigned to exactly one component. The second term _pins_ each terminal $t_i$ to position $i$ by penalizing any other assignment. Expanding the one-hot term using $x^2 = x$: - $ Q_(u k+t, u k+t) = -alpha, quad Q_(u k+s, u k+t) = 2 alpha quad (s < t) $ - Terminal pinning adds $alpha$ to the diagonal $Q_(t_i k+s, t_i k+s)$ for $s != i$, canceling the one-hot incentive. + and + $ H_B = sum_((u,v) in E) sum_(s != t) w^+(u,v) x_(u,s) x_(v,t). $ + The implemented QUBO omits the constant $n alpha$ from $H_A$. + + _Correctness._ Deleting a negative edge strictly reduces cost and cannot reconnect terminals, so every source optimum deletes all negative edges. Both Hamiltonian terms are nonnegative for every binary assignment. A pinned one-hot assignment exists and has energy at most $sum_e w^+(e) < alpha$; any constraint violation costs at least $alpha$. Thus every target optimum is pinned and one-hot. - The cut-cost Hamiltonian: - $ H_B = sum_((u,v) in E) sum_(s != t) w(u,v) dot x_(u,s) dot x_(v,t) $ - counts the total weight of edges whose endpoints lie in different components. + Given any feasible deletion set, label each remaining connected component by its terminal, choosing any label for components without a terminal. Every nonnegative edge crossing labels was already deleted. Conversely, deleting all negative edges and every edge crossing labels separates the terminals. These two directions show that the minimum partition cost plus $C_-$ is exactly the minimum source cost. Consequently every QUBO optimum recovers a source optimum, whose value is the QUBO optimum plus $n alpha + C_-$. All source instances are feasible because deleting all edges separates distinct terminals. - _Correctness._ ($arrow.r.double$) A valid multiway cut with cost $C$ maps to a QUBO solution with $H_A = 0$ (valid partition with correct terminal pinning) and $H_B = C$. ($arrow.l.double$) If $H_A > 0$, the penalty $alpha > sum_e w(e)$ exceeds the entire cut-cost range, so any QUBO minimizer has $H_A = 0$, encoding a valid partition. Among valid partitions, $H_B$ equals the cut cost, and the minimizer achieves the minimum multiway cut. + _Solution extraction._ Find the selected label of each vertex. Delete edge $(u,v)$ exactly when $w(u,v) < 0$ or its endpoint labels differ. Extraction assumes an optimal target witness; it does not check the one-hot constraints again. Integer coefficient overflow is reported by construction. - _Solution extraction._ For each vertex $u$, find terminal position $t$ with $x_(u,t) = 1$. For each edge $(u,v)$, output 1 (cut) if $u$ and $v$ are in different components, 0 otherwise. ] #reduction-rule("GraphPartitioning", "QUBO")[ @@ -12616,8 +12558,8 @@ where $P$ is a penalty weight large enough that any constraint violation costs m "pred solve bundle.json", "pred evaluate qubo.json --config " + cli-config(qubo_ilp_sol.source_config), ) - Source: $n = #qubo_ilp.source.instance.num_vars$ binary variables, 3 off-diagonal terms \ - Target: #qubo_ilp.target.instance.variables.len() ILP variables ($#qubo_ilp.source.instance.num_vars$ original $+ #(qubo_ilp.target.instance.variables.len() - qubo_ilp.source.instance.num_vars)$ auxiliary), #qubo_ilp.target.instance.constraints.len() McCormick constraints \ + Source: $n = #qubo_ilp.source.instance.matrix.nrows$ binary variables, 3 off-diagonal terms \ + Target: #qubo_ilp.target.instance.variables.len() ILP variables ($#qubo_ilp.source.instance.matrix.nrows$ original $+ #(qubo_ilp.target.instance.variables.len() - qubo_ilp.source.instance.matrix.nrows)$ auxiliary), #qubo_ilp.target.instance.constraints.len() McCormick constraints \ Canonical optimal witness: $bold(x) = (#fmt-values(qubo_ilp_sol.source_config))$ #sym.checkmark ], )[ @@ -14005,7 +13947,7 @@ The following reductions to Integer Linear Programming are straightforward formu "pred solve bundle.json", "pred evaluate tsp.json --config " + cli-config(tsp_qubo_sol.source_config), ) - *Step 1 -- Encode each tour position as a binary variable.* A tour is a permutation of $n$ vertices. Introduce $n^2 = #tsp_qubo.target.instance.num_vars$ binary variables $x_(v,p)$: vertex $v$ is at position $p$. + *Step 1 -- Encode each tour position as a binary variable.* A tour is a permutation of $n$ vertices. Introduce $n^2 = #tsp_qubo.target.instance.matrix.nrows$ binary variables $x_(v,p)$: vertex $v$ is at position $p$. $ underbrace(x_(0,0) x_(0,1) x_(0,2), "vertex 0") #h(4pt) underbrace(x_(1,0) x_(1,1) x_(1,2), "vertex 1") #h(4pt) underbrace(x_(2,0) x_(2,1) x_(2,2), "vertex 2") $ *Step 2 -- Penalize invalid permutations.* The penalty $A = 1 + |w_(01)| + |w_(02)| + |w_(12)| = 1 + 1 + 2 + 3 = 7$ ensures any row/column constraint violation outweighs any tour cost. Row constraints (each vertex at exactly one position) and column constraints (each position has one vertex) contribute diagonal $-7$ and off-diagonal $+14$ within each group.\ @@ -14019,15 +13961,18 @@ The following reductions to Integer Linear Programming are straightforward formu )[ Position-based QUBO encoding @lucas2014 maps a Hamiltonian tour to $n^2$ binary variables $x_(v,p)$, where $x_(v,p) = 1$ iff city $v$ is visited at position $p$. The QUBO Hamiltonian $H = H_A + H_B + H_C$ combines permutation constraints with the distance objective ($n^2$ variables indexed by $v dot n + p$). ][ - _Construction._ For graph $G = (V, E)$ with $n = |V|$ and edge weights $w_(u v)$. Let $A = 1 + sum_((u,v) in E) |w_(u v)|$ be the penalty coefficient. + _Construction._ For $n >= 3$, discard self-loops and retain one cheapest edge per endpoint pair; a Hamiltonian cycle on at least three vertices uses neither a loop nor two parallel edges. Let $b = min(0, min_e w(e))$, using $b=0$ if no edges remain. Define $c(e)=w(e)-b >= 0$ and $A=1+sum_e c(e)$. + + _Variables:_ Binary $x_(v,p)$ indicates that vertex $v$ occupies position $p$, with index $v n+p$. The Hamiltonian is + $ H = A sum_v (1-sum_p x_(v,p))^2 + A sum_p (1-sum_v x_(v,p))^2 + sum_(u= A-2n A$, return source infeasibility. Otherwise the source optimum is $E+2n A+n b$. Read the unique vertex at each position and select the stored cheapest edge between consecutive vertices. This maps every qualifying optimum; extraction does not certify permutation or edge feasibility again. Checked construction arithmetic reports coefficients that cannot be represented in the integer QUBO. - _Correctness._ ($arrow.r.double$) A valid tour defines a permutation matrix satisfying $H_A = H_B = 0$; the $H_C$ terms sum to the tour cost. ($arrow.l.double$) The minimum-energy state has $H_A = H_B = 0$ (penalty $A$ exceeds any tour cost), so it encodes a valid permutation; $H_C$ equals the tour cost, selecting the shortest tour. + _Small instances._ The source witness is a connected edge set with degree two at every vertex. For $n=1$ its optimum is the cheapest self-loop, if one exists. For $n=2$ it is the two cheapest parallel edges joining the vertices, if two exist. For $n=0$ the source has no cycle. These cases are solved during construction by selecting the one or two smallest relevant edge weights in linear time. The target is a zero objective on $n^2$ variables; every target optimum maps to the stored source optimum or infeasibility. This preserves the source model's accepted graph domain. - _Solution extraction._ From QUBO solution $x^*$, for each position $p$ find the unique vertex $v$ with $x^*_(v n + p) = 1$. Map consecutive position pairs to edge indices. ] #let lcs_mis = load-example("LongestCommonSubsequence", "MaximumIndependentSet") @@ -15233,24 +15178,6 @@ The following reductions to Integer Linear Programming are straightforward formu _Solution extraction._ For each position $p$, return the unique $i$ with $x_(i,p)=1$, using the existing one-hot decoder. There are $m^2+m^3$ binary variables and $2m+3m^3+m r$ constraints, where $r$ is the number of unreachable ordered required-arc pairs; hence at most $2m+4m^3$ constraints. ] -#reduction-rule("SteinerTreeInGraphs", "ILP")[ - Select edges and certify terminal connectivity by sending one unit of flow from a root terminal to every other terminal through the selected subgraph. -][ - _Construction._ Fix a root terminal $r in R$. Variables: binary $y_(u,v)$ for each undirected edge $\{u,v\}$ and nonnegative flow variables $f^t_(u,v)$ on each directed edge orientation for every terminal $t in R backslash {r}$. The ILP is: - $ - min quad & sum_({u,v} in E) w_(u,v) y_(u,v) \ - "subject to" quad & sum_(u) f^t_(u,v) - sum_(w) f^t_(v,w) = b_(t,v) quad forall t in R backslash {r}, v in V \ - & f^t_(u,v) <= y_(u,v) quad forall {u, v} in E, t in R backslash {r} \ - & f^t_(v,u) <= y_(u,v) quad forall {u, v} in E, t in R backslash {r} \ - & y_(u,v) in {0, 1}, f^t_(u,v) in ZZ_(>=0), - $ - where $b_(t,v) = -1$ if $v = r$, $b_(t,v) = 1$ if $v = t$, and $b_(t,v) = 0$ otherwise. - - _Correctness._ ($arrow.r.double$) A Steiner tree supports a unit flow from the root to every other terminal using exactly its selected edges, with the same total weight. ($arrow.l.double$) Any feasible ILP solution selects a connected subgraph spanning all terminals, and with nonnegative edge weights an optimum solution is a minimum-weight Steiner tree. - - _Solution extraction._ Output the binary edge-selection vector $(y_e)_(e in E)$. -] - // Scheduling #reduction-rule("FlowShopScheduling", "ILP")[ @@ -16609,12 +16536,14 @@ Problems parameterized by graph type, weight type, target type, or clause width _Solution extraction._ Return the target configuration unchanged. ] +The numerical variant embeddings below preserve individual stored coefficients or coordinates. Their algebraic identities describe the formal objectives. Floating-point model evaluation still follows finite `f64` arithmetic and can round intermediate expressions; a lossless scalar embedding does not certify backend optimality. CVP instead evaluates its stored coordinates with exact rational squared distances. + #reduction-rule("SpinGlass", "SpinGlass")[ An Ising spin-glass instance with integer couplings and fields ($J_(i j), h_i in ZZ$) converts to the floating-point variant ($J_(i j), h_i in RR$) through exact `i64_to_exact_f64` embeddings. The graph topology is preserved. ][ _Construction._ Given $"SpinGlass"(G, bold(J), bold(h))$ with $J_(i j) in ZZ$ and $h_i in ZZ$, construct $"SpinGlass"(G, bold(J)', bold(h)')$ with $J'_(i j) = J_(i j) in RR$ and $h'_i = h_i in RR$. - _Correctness._ The spin-glass Hamiltonian $H(bold(s)) = sum_((i,j) in E) J_(i j) s_i s_j + sum_i h_i s_i$ is preserved exactly under the integer-to-float embedding (no rounding). Spin configurations and the objective value are unchanged. + _Correctness._ The spin-glass Hamiltonian $H(bold(s)) = sum_((i,j) in E) J_(i j) s_i s_j + sum_i h_i s_i$ is the same formal Hamiltonian under the coefficient embedding. Spin configurations are unchanged. _Solution extraction._ Return the target configuration unchanged. ] @@ -16634,9 +16563,9 @@ Problems parameterized by graph type, weight type, target type, or clause width #reduction-rule("ClosestVectorProblem", "ClosestVectorProblem")[ An integer-target CVP instance converts to the floating-target variant by embedding every target coordinate with `i64_to_exact_f64`. The integer lattice basis is copied unchanged. ][ - _Construction._ Given $(B, bold(t))$ with $B in ZZ^(m times n)$ and $bold(t) in ZZ^m$, construct $(B, bold(t)')$ with $t'_i = "f64"(t_i)$ for every exactly representable coordinate $|t_i| lt.eq 2^53 - 1$. + _Construction._ Given $(B, bold(t))$ with $B in ZZ^(m times n)$ and $bold(t) in ZZ^m$, construct $(B, bold(t)')$ with $t'_i = "f64"(t_i)$ when every target coordinate satisfies $abs(t_i) <= 2^53 - 1$, the supported conversion range. - _Correctness._ Exact coordinate conversion gives $bold(t)' = bold(t)$ in $RR^m$. Therefore $norm(B bold(x) - bold(t)')_2 = norm(B bold(x) - bold(t))_2$ for every $bold(x) in ZZ^n$, so the minimizers coincide. + _Correctness._ Exact coordinate conversion gives $bold(t)' = bold(t)$ in $RR^m$. Therefore $norm(B bold(x) - bold(t)')_2^2 = norm(B bold(x) - bold(t))_2^2$ for every $bold(x) in ZZ^n$, so the minimizers coincide. _Solution extraction._ Return the integer coefficient vector unchanged. ] @@ -16644,7 +16573,7 @@ Problems parameterized by graph type, weight type, target type, or clause width #reduction-rule("QUBO", "QUBO")[ An integer QUBO converts to the floating-coefficient variant by embedding every matrix coefficient with `i64_to_exact_f64`. ][ - _Construction._ Given $Q in ZZ^(n times n)$, construct $Q' in RR^(n times n)$ with $Q'_(i j) = "f64"(Q_(i j))$ for every exactly representable coefficient $|Q_(i j)| lt.eq 2^53 - 1$. + _Construction._ Given $Q in ZZ^(n times n)$, construct $Q' in RR^(n times n)$ with $Q'_(i j) = "f64"(Q_(i j))$ when every matrix coefficient satisfies $abs(Q_(i j)) <= 2^53 - 1$, the supported conversion range. _Correctness._ For every binary vector $bold(x)$, exact coefficient conversion gives $bold(x)^top Q' bold(x) = bold(x)^top Q bold(x)$. The objective ordering and minimizers are preserved. @@ -16907,11 +16836,11 @@ The following table shows concrete target-variable counts for example instances, #reduction-rule("ILP", "ILP")[ ILP variants convert between binary and bounded integer variable domains and between exact-integer and floating-point coefficients. Binary variables embed directly into integer variables. A finitely bounded integer variable is encoded by binary variables with truncated positional weights. Integer coefficients are embedded only when every stored coefficient and right-hand side has an exact `f64` representation. ][ - _Construction._ For the binary-to-integer edge, copy the variables, constraints, objective, and optimization direction unchanged. For an integer variable $x_i in [L_i, U_i]$, let $D_i = U_i - L_i$ and choose positive truncated binary weights $w_(i j)$ whose subset sums represent every integer from $0$ through $D_i$; substitute $x_i = L_i + sum_j w_(i j)y_(i j)$ into every constraint and objective term. This edge rejects variables without two finite bounds. For the coefficient edge, copy the variable bounds and optimization direction and convert each entry of the constraint matrix, right-hand side, and objective independently; reject the instance if any integer lies outside the exactly representable `f64` integer range. + _Construction._ For the binary-to-integer edge, copy the variables, constraints, objective, and optimization direction unchanged. For an integer variable $x_i in [L_i, U_i]$, let $D_i = U_i - L_i$ and choose positive truncated binary weights $w_(i j)$ whose subset sums represent every integer from $0$ through $D_i$; substitute $x_i = L_i + sum_j w_(i j)y_(i j)$ into every constraint and objective term. This edge rejects variables without two finite bounds. For the coefficient edge, copy the variable bounds and optimization direction and convert each entry of the constraint matrix, right-hand side, and objective independently; reject the instance if any converted integer is outside the supported range $[-(2^53 - 1), 2^53 - 1]$. _Correctness._ The binary-to-integer embedding changes no mathematical expression. For bounded integer variables, every $x_i in [L_i,U_i]$ has a truncated binary representation, and every binary assignment decodes inside that interval; substitution preserves all constraints and objective values. Exact conversion preserves every stored coefficient, so it constructs the same formal linear objective and constraints over the same integer variables. - _Solution extraction._ Binary-to-integer and coefficient conversions preserve the assignment; coefficient conversion additionally checks the assignment against the source integer ILP. Binary encoding returns $x_i = L_i + sum_j w_(i j)y_(i j)$. + _Solution extraction._ Binary-to-integer and coefficient conversions preserve the assignment after the standard target-solution validation. Numerical solver accuracy is independent of the mathematical coefficient conversion. Binary encoding returns $x_i = L_i + sum_j w_(i j)y_(i j)$. ] #let hc_hp = load-example("HamiltonianCircuit", "HamiltonianPath") @@ -17263,7 +17192,7 @@ The following table shows concrete target-variable counts for example instances, *Multiplicity:* The fixture stores one canonical Hamiltonian circuit. Rotating or reversing that same cycle yields equivalent target witnesses with the same extracted cover. ], )[ - Garey and Johnson's Theorem 3.4 replaces each source edge by a 12-vertex cover-testing gadget and uses $k$ selector vertices to choose $k$ source vertices whose incident gadget-paths together cover every gadget @garey1979. In the unit-weight decision setting, the constructed graph is Hamiltonian iff the source graph has a vertex cover of size at most $k$. + Garey and Johnson's Theorem 3.4 replaces each source edge by a 12-vertex cover-testing gadget and uses $k$ selector vertices to choose $k$ source vertices whose incident gadget-paths together cover every gadget @garey1979. The registered source uses the `One` weight variant of Decision Minimum Vertex Cover. The constructed graph is Hamiltonian iff the source graph has a vertex cover of size at most $k$. ][ _Construction._ Let the source be a unit-weight Decision Minimum Vertex Cover instance $(G = (V, E), k)$ with $G$ simple. For each edge $e = {u, v} in E$, create a gadget with vertices $(u, e, i)$ and $(v, e, i)$ for $1 <= i <= 6$. Add the two 6-chains on the $u$-side and $v$-side together with the four cross edges ${(u, e, 3), (v, e, 1)}$, ${(v, e, 3), (u, e, 1)}$, ${(u, e, 6), (v, e, 4)}$, and ${(v, e, 6), (u, e, 4)}$. For every source vertex $v$, order its incident edges as $e_(v[1]), dots, e_(v[deg(v)])$ and connect ${(v, e_(v[i]), 6), (v, e_(v[i+1]), 1)}$ for $1 <= i < deg(v)$, forming one path that contains exactly the gadget copies labeled by $v$. Finally add selector vertices $a_1, dots, a_k$ and join each selector to both endpoints of every non-isolated vertex-path. Thus the theorem branch has $k + 12|E|$ vertices and $14|E| + sum_(v in V^+) (deg(v)-1) + 2k|V^+|$ edges, where $V^+ = {v in V : deg(v) > 0}$. @@ -19075,9 +19004,9 @@ The following table shows concrete target-variable counts for example instances, ($arrow.r.double$) Given a perfect matching $M'$, form one ABCD group for every source triple. If $m_l in M'$, combine $u_l$ with the unique first-occurrence $B$, $C$, and $D$ items of coordinates $(a_l, b_l, c_l)$; otherwise combine $u_l$ with the corresponding later-occurrence dummy items. Because $r = 32 q$ prevents carries between the $r$, $r^2$, $r^3$, and $r^4$ digits, every such group sums to $T_1$, so the tagged instance has a 4-partition. For each tagged 4-set choose any two members $a_i, a_j$ and let the other two be $a_k, a_l$. Then ${w_i, w_j, u_(i j)}$ and ${w_k, w_l, u'_(i j)}$ both sum to $B$. Every pairing gadget not used this way joins one filler in a triple ${u_(i j), u'_(i j), 20 T_2}$. Hence the produced 3-Partition instance is feasible. - ($arrow.l.double$) In any feasible target solution every number lies strictly between $B / 4$ and $B / 2$, so the partition really is into triples. Modulo 4, regular numbers are congruent to 1, pairing numbers to 2, and fillers to 0. Therefore every triple is either of type $(1, 1, 2)$ or $(0, 2, 2)$. The $(0, 2, 2)$ triples identify the unused pairing gadgets, leaving a family of $(1, 1, 2)$ triples that reconstructs a 4-partition of the tagged numbers. Since $1 + 2 + 4 + 8 equiv 15 mod 16$, every recovered tagged 4-set contains exactly one former $A$-, $B$-, $C$-, and $D$-item. The carry-free base-$r$ encoding then forces each ABCD group to be either a real group (all first occurrences) or a dummy group (all later occurrences). The real groups pick exactly $q$ source triples, one for each coordinate of $W$, $X$, and $Y$, so they form a perfect 3-dimensional matching. + ($arrow.l.double$) In any feasible target solution every number lies strictly between $B / 4$ and $B / 2$, so the partition really is into triples. Modulo 4, regular numbers are congruent to 1, pairing numbers to 2, and fillers to 0. Therefore every triple is either of type $(1, 1, 2)$ or $(0, 2, 2)$. First normalize the $(0, 2, 2)$ triples as in @garey1979: if a filler shares a triple with pairing elements $p, q$, exchange $q$ with the original mate of $p$. Both have the same size, since every original pair sums to $44 T_2 + 4 = B - 20 T_2$, so both affected triples remain valid. Each exchange fixes a filler triple without disturbing a previously fixed one. After normalization, every remaining original pair occurs in two $(1, 1, 2)$ triples. Their four actual regular elements sum to $2 B - (44 T_2 + 4) = 84 T_2 + 4$, so the corresponding tagged numbers sum to $T_2$. These disjoint four-sets reconstruct a 4-partition. Since $1 + 2 + 4 + 8 equiv 15 mod 16$, every recovered tagged 4-set contains exactly one former $A$-, $B$-, $C$-, and $D$-item. The carry-free base-$r$ encoding then forces each ABCD group to be either a real group (all first occurrences) or a dummy group (all later occurrences). The real groups pick exactly $q$ source triples, one for each coordinate of $W$, $X$, and $Y$, so they form a perfect 3-dimensional matching. - _Solution extraction._ Reverse the 4-Partition $arrow.r$ 3-Partition gadget by pairing each triple containing some $u_(i j)$ with the unique triple containing the matching $u'_(i j)$. This recovers the tagged 4-set. Undo the mod-16 tags to obtain one ABCD group, discard every dummy group whose $B$, $C$, and $D$ items are not first occurrences, and read the selected source triple from the surviving $A$-item. + _Solution extraction._ Normalize filler triples by the equal-size exchanges above, maintaining each element's current group and position. Then pair the remaining triples containing original mates $u_(i j), u'_(i j)$ and collect their four actual regular elements; their indices need not equal the indices used to construct the pairing gadget. The normalization and pairing take linear time in the target element count. Undo the mod-16 tags to obtain one ABCD group, discard every dummy group whose $B$, $C$, and $D$ items are not first occurrences, and read the selected source triple from the surviving $A$-item. ] #let tdm_ilp = load-example("ThreeDimensionalMatching", "ILP") @@ -19556,36 +19485,22 @@ The following table shows concrete target-variable counts for example instances, )[ Bienstock, Goemans, Simchi-Levi, Williamson @BienstockGoemansSimchiLeviWilliamson1993 introduced the prize/penalty framework for prize-collecting network design; Tuncbag and coauthors @TuncbagEtAl2013PCSF @TuncbagEtAl2012RECOMB used the same artificial-root idea to translate PCSF into a rooted prize-collecting Steiner tree on biological networks. The combined construction recorded here adds a per-vertex auxiliary-terminal gadget that compiles the remaining omitted-prize term `beta * p(v)` into ordinary Steiner-tree edge costs, so the target is a plain (unweighted-prize) Steiner Tree instance. ][ - _Construction._ Given a PCSF instance with graph $G = (V, E)$, edge costs $c$, vertex prizes $p$, and parameters $beta >= 0$, $omega >= 0$, let $V_p = {v in V : p(v) > 0}$ and $k = |V_p|$. Build the target graph $H = (V_H, E_H)$ with weights $c_H$ and terminal set $T_H$ as follows. - - 1. Add a fresh artificial root $r$: $V_H = V union {r} union {t_v : v in V_p}$. - 2. Keep every original edge $e in E$ with $c_H(e) = c(e)$. - 3. For every $v in V$, add a root-attachment edge $(r, v)$ with $c_H((r, v)) = omega$. - 4. For every prized vertex $v in V_p$, add an include-edge $(v, t_v)$ with cost $0$ and an omit-edge $(r, t_v)$ with cost $beta dot p(v)$. - 5. Set $T_H = {r} union {t_v : v in V_p}$. Original vertices $V$ and the new gadget terminals coexist; only $r$ and the $t_v$ are terminals. - - Solve $"SteinerTree"(H, c_H, T_H)$ to obtain a minimum-weight tree $T^*$ spanning $T_H$. - - _Witness extraction._ From $T^*$ recover the PCSF witness $(V_F, E_F)$ by - - $ E_F = T^* inter E(G), quad V_F = { v in V : (v, t_v) in T^* } union { "endpoints of edges in" E_F }. $ + _Construction._ Given a PCSF instance with graph $G=(V,E)$, nonnegative edge costs $c$, nonnegative prizes $p$, and $beta, omega >= 0$, let $V_p={v in V:p(v)>0}$, $k=|V_p|$, and $M=omega+1$. Add an artificial root $r$ and one auxiliary terminal $t_v$ for each $v in V_p$. Keep every original edge with its cost, add $(r,v)$ of cost $omega$ for every original vertex, and add $(v,t_v)$ of cost $M$ and $(r,t_v)$ of cost $M+beta p(v)$. The terminal set is ${r} union {t_v:v in V_p}$. - Equivalently, deleting $r$ and the gadget vertices ${t_v}$ from $T^*$ leaves a disjoint union of trees on $V$; $V_F$ is the set of original vertices touched by this restricted forest, and $E_F$ is exactly $T^* inter E(G)$. Both directions are consistent because: + _Forward bound._ Given any source forest $F$, attach each component once to $r$. For each prized vertex, choose its include edge if selected and its omit edge otherwise. The auxiliary terminals are leaves; the result is a tree spanning all terminals, with cost $f(F)+k M$. Thus $"OPT"_T <= "OPT"_F+k M$. - - any prized vertex $v$ in $V_F$ pays the cost-$0$ include-edge $(v, t_v)$ to reach $t_v$ inside $T^*$; - - any prized vertex $v$ omitted from $V_F$ has $t_v$ joined to the tree exclusively through $(r, t_v)$, paying $beta dot p(v)$. + _Reverse bound._ In an optimal target tree, every auxiliary terminal is a leaf. If both edges at $t_v$ were selected, delete $(r,t_v)$ and add $(r,v)$. The latter edge cannot already be selected, since those three edges would form a cycle. The replacement reconnects the two components created by deletion and decreases cost by $M+beta p(v)-omega >= 1$, a contradiction. - _Correctness._ ($arrow.r.double$) Given any feasible source forest $F$, attach each connected component of $F$ to $r$ via exactly one root-attachment edge (cost $omega$ per component) and resolve each gadget locally: take $(v, t_v)$ if $v in V_F$, else $(r, t_v)$. The resulting subgraph of $H$ is connected, spans $T_H$, and is a tree because every gadget is paid by exactly one of its two edges and the only chord that could close a cycle is removed by the choice of a single root-attachment edge per component. Its cost equals + Extract original selected edges, their endpoints, and each prized vertex whose include edge is selected. This is a feasible source forest. After deleting the auxiliary leaves, each original component has exactly one root attachment, by connectivity and acyclicity. Extraction may discard isolated zero-prize vertices, which cannot increase cost because $omega>=0$. Every omitted positive prize has its omit edge selected; omit edges at vertices retained by original edges only add nonnegative target cost. Therefore $f(F) <= "cost"(T)-k M$. Together with the forward bound, this proves $"OPT"_T="OPT"_F+k M$ and optimality of every extracted optimal target witness. - $ sum_(e in E_F) c(e) + omega dot kappa(F) + beta dot sum_(v in.not V_F) p(v) + 0 = f'(F). $ + _Witness extraction._ Return + $ E_F=T inter E(G), quad V_F={v:(v,t_v) in T} union {"endpoints of edges in" E_F}. $ + No source optimization is performed during extraction. - ($arrow.l.double$) Conversely, given an optimal Steiner tree $T^*$, the restriction $E_F = T^* inter E(G)$ is acyclic (subset of a tree) and respects the PCSF feasibility constraint that selected edges only touch selected vertices, because every endpoint $v$ of an edge in $E_F$ is forced into $V_F$ by the extraction rule. Each connected component of $F$ corresponds to a maximal subtree of $T^*$ confined to $V$, and any optimal $T^*$ uses exactly one root-attachment edge per component (a second incident root edge could be replaced by a cheaper internal path, contradicting optimality). Each prized vertex $v in V_F$ is reached by $T^*$ via original edges, so the include-edge $(v, t_v)$ is selected for free; each omitted prized vertex contributes the omit-edge $(r, t_v)$ of cost $beta dot p(v)$. Summing the contributions reproduces $f'(F)$, so $"cost"_H(T^*) = f'(F^*)$ at optima and the extracted forest is optimal for PCSF. + _Overhead._ The exact counts remain $|V_H|=n+k+1$, $|E_H|=m+n+2k$, and $|T_H|=k+1$. Coefficients are computed with checked integer arithmetic in the native representation. - _Overhead._ With $n = |V|$, $m = |E|$, and $k = |V_p|$: - $ |V_H| = n + k + 1, quad |E_H| = m + n + 2 k, quad |T_H| = k + 1. $ - Every quantity is linear in the source instance size, so the reduction is a polynomial-time transformation. + _Boundary cases._ With no positive prizes, only $r$ is a terminal and its zero-edge tree maps to the empty forest. The proof also covers $beta=0$ and $omega=0$, including ties. For two adjacent vertices with edge cost zero, prizes $(1,2)$, $beta=1$, and $omega=5$, the source optimum is the empty forest of cost $3$; the target optimum selects both omit edges and costs $3+2 dot 6=15$. - _Remark._ The artificial-root edges all share cost $omega$. Tuncbag et al. originally used this construction with $omega = c$ for any positive scalar $c$ acting as a per-component penalty; we follow that convention. When $omega = 0$, root-attachment edges become free and the construction degenerates: any rooted spanning tree of the prized-vertex closure achieves the same cost, but the witness-extraction recipe still recovers a feasible (cost-equivalent) PCSF forest, possibly with a different component count. ] #pagebreak() diff --git a/docs/src/design.md b/docs/src/design.md index 54ed4f47f..69e93cb60 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -21,7 +21,7 @@ choosing numeric fields or implementing arithmetic in a model or reduction. ## Problem Model -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. +Every problem implements `Problem`. The associated `Value` type is the per-configuration aggregate returned by `evaluate()`. The brute-force solver folds these values across the configuration space and uses its `SolutionAggregate` capability to select corresponding witnesses. Specialized solvers and ILP backends return their solutions directly; model evaluation does not require that selection capability. ```rust,ignore trait Problem: Clone { @@ -37,7 +37,7 @@ trait Problem: Clone { ``` - **`Problem`** — the base trait. Every problem declares a mathematical `Solution` type, evaluates that type directly, and reports its canonical instance parameters. For example, a 4-vertex MIS uses `Vec`; `evaluate(&[true, false, true, false])` returns `Ok(Max(Some(2)))` if vertices 0 and 2 form an independent set, or `Ok(Max(None))` if they share an edge. Inherent getters such as `num_vertices()` and `num_edges()` supply the named parameters used by reduction expressions. -- **`BruteForceProblem`** — the reference-solver capability for registered variants with a finite Cartesian coordinate space. Its `dimensions()` method and the Cartesian iterator belong to the brute-force solver, not to the mathematical `Problem` contract. +- **`BruteForceProblem`** — the reference-solver capability for registered variants with a finite Cartesian coordinate space. Its fallible `num_variables()` and `dimension(variable)` methods describe coordinates without allocating their vector. These methods and the Cartesian iterator belong to the brute-force solver, not to the mathematical `Problem` contract. - **Objective problems** — typically use `Max`, `Min`, or `Extremum` as `Value`. - **Feasibility problems** — typically use `Or`. - **Solve contract** — a successful solve always returns the problem's `Solution`; a global count or statistic without a representative solution is not a `Problem` solve. @@ -62,6 +62,26 @@ an undeclared weight or length input is an error, even when every value is one. calls `P`'s registered constructor before wrapping the result. It does not repeat the inner input schema or deserialize construction inputs as persisted model JSON. +### Construction and deserialization + +Model constructors own instance checks and normalization. Fallible constructors +return `Result`; Serde calls those constructors through +`#[serde(try_from = "...")]` or a manual `Deserialize` implementation. A model's +`CreateSpec` handles its input names, defaults, and inference, then calls the same +constructor. Nested graphs validate their own endpoints. + +For example, `MaximumIndependentSet::new(SimpleGraph::path(3), vec![1, 1])` +returns an error because three vertices require three weights. Loading the same +instance from JSON also fails during construction, before evaluation or solving. +Supplying three weights creates the same mathematical instance through either +entry point. A reduction propagates target construction failures with +`ReduceTo::target_construction`, preserving the source and target model types. + +Cached dimensions, adjacency lists, and other derived fields are rebuilt from +validated inputs during deserialization. Persisted cache values do not override +those computations. Setters that can violate an instance invariant check their +replacement data before assigning it; failure leaves the instance unchanged. + ## Numeric types and arithmetic Numeric formats are selected by semantic role: @@ -93,39 +113,183 @@ SpinGlass couplings and its objective result use `i64`, while the temporary temporary calculations are also outside the contract, but numeric fields written into its target model must follow the target model's numeric format. -Weight variants are `One`, `i64`, and `f64`, with `One ⊂ i64 ⊂ f64`. -`i64 → f64` is a fallible reduction using a checked conversion in -`±(2^53-1)`, not `as f64`. +Supported weight variants are `One`, `i64`, and `f64`. + +### Responsibility boundaries + +| Layer | Contract | +|-------|----------| +| Model (`Problem`) | Defines instances, witnesses, feasibility, and objectives in its declared mathematical representation. Evaluation is independent of backend tolerances, statuses, and enumeration capacity. | +| Reduction (`ReduceTo`, `ReductionResult`) | Constructs the target within the rule's mathematical domain and maps target witnesses satisfying the stated preconditions to source witnesses. It owns coefficient arithmetic, parameter relationships, and mapping correctness. | +| Backend adapter | Encodes the target, executes the backend, interprets statuses, decodes numerical results, and validates the returned witness against the original target model. | +| Solver orchestration | Executes registered capabilities and reduction chains, interprets aggregate results, and extracts source witnesses under the reduction contracts. | +| CLI / MCP | Uses public construction, evaluation, and solving APIs and presents their results. | + +Models and rules do not repair backend results, change constraints to make a +solver succeed, or independently prove a backend's global optimality. Invalid +returned witnesses and operational failures must be explicit errors. A backend's +numerical limitations do not justify a package-wide certificate system or +downgrading every successful result. + +Search-space cardinalities belong to the solver capability, not the mathematical +model. Actual model storage and witness representation constraints still apply. + +### Witness and aggregate reductions + +`ReductionResult::extract_solution()` maps `Target::Solution` to +`Source::Solution`; it does not require equal `Problem::Value` types. Resolve +concrete associated types from the implementation, then check the mathematical +mapping and its Rust implementation rather than applying a wrapper-pair whitelist. + +For an optimization reduction, explain why target optima map to source optima. +Opposite directions are valid when the objective relationship reverses order: +independent-set size `k` corresponds to vertex-cover size `n-k` by complementing +the witness. Different numeric value types do not require conversion of an +objective that the extractor never converts. Check the domain and arithmetic of +conversions the construction or mapping actually performs. + +Value-only operations use `ReduceToAggregate` / `AggregateReductionResult` and +must justify their actual `extract_value()` relationship. Multi-query algorithms +use the existing Turing reduction capability. A feasibility witness alone does +not establish an optimization result without the required mathematical argument. + +`Problem::evaluate()` defines feasibility as well as objective values. A successful +call can return an infeasible value such as `Or(false)` or `Max(None)`; absence +of an `EvaluationError` does not imply a valid witness. The adapter validates +backend output before returning it. Both typed extraction and `pred extract` +assume witnesses satisfying the reduction's documented premises; neither checks +feasibility or optimality. JSON parsing and type conversion remain at the transport +boundary. Evaluation may supply requested display values without acting as an +acceptance gate. Solver orchestration interprets aggregate mappings to determine +source outcomes before invoking witness mappings. + +### Executed reduction lifecycle + +A witness reduction is one algorithm with construction and reverse mapping. +`reduce_to()` returns the target and all mapping state in one result. Each +executed chain step constructs that result once. Its witness and optional +aggregate `Rc` views share one allocation; obtaining another view does not +reconstruct or copy the target. `Decision

-> P` stores the bound with that +same result. + +For every rule, document its instance domain, required target witness quality +and conditions, source guarantee, and treatment of source infeasibility. +The guarantee applies to every qualifying witness, including tied optima. +A witness-capable edge alone does not establish a complete-solving procedure: +composition must establish the preceding edge's witness premise. + +| Example | Required recovery | +|---|---| +| MVC -> MIS | Complement a maximum independent set to obtain a minimum cover | +| SAT -> MIS | With `m` clauses, optimum size `m` permits witness extraction; an optimum below `m` means UNSAT | +| Binary ILP -> QUBO | Use the constructed energy relationship to obtain a source optimum or source infeasibility; a QUBO optimum alone does not establish ILP feasibility | +| MVC -> MIS -> SetPacking -> ILP | Apply the stored ILP-to-packing and packing-to-MIS mappings, then the complement mapping | +| TSP -> QUBO | Shift signed edge costs uniformly; the energy threshold distinguishes source infeasibility, and the stored offset recovers tour cost | +| Discrete inverse kinematics -> QUBO | Restore omitted constants and compare against the gap between feasible distance and constraint penalties before decoding orientations | +| MultiwayCut -> QUBO | Always delete negative edges; optimize nonnegative cut cost and decode an optimal terminal partition | +| Aggregate-only operation | Map the final value without selecting any witness, including `Sum` | + +The mathematical thresholds and objective relationships belong to the rule. +Solver completion invokes the executed step's concrete `interpret_optimum` +operation before its witness mapping. This operation shares the constructed +result and does not query the model registry. Ordinary extraction uses only the +witness mapping. Typed chain, executed path, and JSON extraction share the same +reverse traversal; dynamic/JSON methods perform necessary representation +conversion rather than introducing another extraction contract. + +`SolutionAggregate` is defined in `solvers/brute_force.rs` and exported through +`solvers` for enumeration clients. It compares candidate and aggregate values; +it is not a model-feasibility interface. Concrete variant declarations generate +`DynProblem` transport implementations using the value's own `is_valid` +semantics, without aggregation or solver-registration requirements. A concrete +hand-registered dynamic type can use `impl_dyn_problem!` directly. + +Witness and aggregate describe what can be recovered. Turing describes a +potentially adaptive query procedure. Exact witness recovery does not establish +approximation or counting preservation; those require their own proofs. ### Arithmetic -- Keep arithmetic in the declared type. Exact values use checked `i64` - operations; approximate values use finite `f64` operations. -- Constructors and reductions reject an arithmetic step that would overflow - `i64` when producing a stored field. They do not cap every magnitude at - `2^53-1`. `evaluate()` never widens, wraps, saturates, or silently - approximates. -- Do not promote an `i64` calculation to `i128`, `BigInt`, or `BigUint` to - accept a larger instance. +- Integer models and reductions preserve integer values in their declared + representation. Report actual arithmetic overflow explicitly; do not wrap, + saturate, or silently approximate. Reuse an existing exact representation + when the mathematical model requires it. +- Floating-point models and rules use ordinary finite `f64` arithmetic and its + rounding. Check non-finite results and do not deliberately discard nonzero + coefficients. Backend feasibility tolerances must not expand the model's + feasible set. A declared input convention, such as checking probability sums, + is distinct from accepting a solver's returned assignment. +- CVP evaluates squared distance as `Min` through its + `squared_distance()` method. Integer coordinates enter exact integer arithmetic; + finite `f64` targets retain their stored binary rational values. For example, + the zero lattice point and target `(3, 4)` have objective `25`. The customized + solver uses the same coordinate conversion. SubsetSum compares squared distance + with its integer item count. JSON evaluation uses the dependency's rational + serialization; CLI display uses fractions such as `Min(9/16)`. +- `i64_to_exact_f64()` accepts integers in `[-(2^53-1), 2^53-1]` and rejects + everything outside that supported conversion range. This is a conservative + interface limit, not the set of all exactly representable f64 integers. Ordinary conversion + into a floating-point model and backend transport are separate + responsibilities. Neither a lossless scalar conversion nor `transform = exact` + proves error-free floating-point evaluation or backend optimality; the latter + describes parameter relationships only. +- Preserve real construction and witness-structure checks, including bounds + derived by the reduction and adjacency preservation in geometric mappings. + Do not add exact arithmetic solely to audit a floating-point backend or reject + a mathematical reduction because that backend may struggle to solve it. ### Boundaries -- Use `From` only for value-preserving conversions and `TryFrom` when range, - sign, or domain can change. Do not use `as` for model-derived values. +- Use value-preserving conversions where possible and checked conversions for + range/sign changes. A floating-point model's declared rounding is not a + lossless-conversion requirement. Reuse `i64_to_exact_f64` where lossless scalar + conversion is actually required; backend input acceptance belongs to the + adapter and must not narrow integer model domains. - Converting a registered parameter getter from `usize` to `u64` is an internal - invariant of `Problem::parameters()`, not a recoverable construction error. A valid - instance's registered parameters must already fit `u64`; the - implementation checks this conversion to prevent silent truncation. -- Symbolic parameter evaluation may use arbitrary-precision integers for local - intermediate arithmetic, but a materialized `ProblemParameters` must fit `u64`. -- An `i64` to `f64` conversion is explicit and fallible: it succeeds only - for `|value| ≤ 2^53-1`. Use one shared helper at weight casts, solver - adapters, and other exact-to-float hubs. -- A lattice-to-`UnitDiskGraph` reduction converts coordinates fallibly and - rejects a stored `f64` geometry that would change source adjacency. -- Rust constructors keep `i64` fields as `i64`. CLI and MCP JSON encoding - of an `i64` with `|value| > 2^53-1` errors; there is no string encoding - and no clamping. + invariant of `Problem::parameters()`, not a recoverable construction error. + Check it to prevent silent truncation. +- Symbolic parameter evaluation may use arbitrary-precision intermediates, but + materialized `ProblemParameters` must fit `u64`. +- A lattice-to-`UnitDiskGraph` reduction must reject a stored geometry that + changes source adjacency. This is a mathematical reduction requirement. +- Rust constructors retain their declared integer fields. Existing serde JSON + serialization can emit i64 integer values beyond the consecutive-integer range + of f64. Describe the actual codec and consumer representation; do not impose + a universal f64 gate on Rust models or claim one exists in CLI/MCP. + +### Validation evidence + +Model tests check definitions and direct evaluation. Reduction tests check +construction, witness mappings, objective relationships, and parameter formulas +using explicit witnesses or small exhaustive enumeration. Choose cases that can +expose a concrete defect; there is no minimum vertex, assertion, test-function, +or generated-check count that establishes correctness. + +Keep representative solver integration tests and report whether failures occur +in construction, solving, extraction, or source validation. Backend timeout or +numerical failure is not evidence that a reduction theorem is false. Test backend +decoding and transport boundaries once in their shared implementation, not in +every rule. Retain arithmetic regressions that detect actual coefficient loss or +incorrect mappings. Do not enlarge tolerances to make a failing test pass. + +### Search representation + +`BruteForceProblem::num_variables()` and `dimension(variable)` return +`Result`. The caller supplies an index below the coordinate +count. Derived counts and cardinalities use checked arithmetic. Shared +`cartesian_dimensions()` materializes these values with fallible allocation for +registered solving and inspection; models do not call it during evaluation. + +The Cartesian iterator advances coordinates until mixed-radix exhaustion. Its +complete search count need not fit `usize`, and it does not implement +`ExactSizeIterator`. An empty product has one empty candidate; any zero-sized +coordinate makes the product empty. Native masks and dense tables retain their +actual representation limits and report errors before overflowing or allocating +an unrepresentable table. These are implementation limits, not difficulty budgets. + +`TruthTable` construction and deserialization share checked row-count and shape +validation. Variable-arity constructors return `ConstructionError` for unsupported +row counts or allocation failures. Valid tables retain the same JSON format. ## Variant System @@ -200,12 +364,17 @@ impl_variant_reduction!( MaximumIndependentSet, => , fields: [num_vertices, num_edges], - |src| MaximumIndependentSet::new( - SimpleGraph::new( - src.num_vertices(), - Graph::edges(src.graph()), - ), - src.weights().to_vec()) + aggregate: identity, + |src| { + let construction_error = ReductionError::construction::< + MaximumIndependentSet, + MaximumIndependentSet, + >; + let graph = SimpleGraph::new(src.num_vertices(), Graph::edges(src.graph())) + .map_err(construction_error)?; + MaximumIndependentSet::new(graph, src.weights().to_vec()) + .map_err(construction_error)? + } ); ``` @@ -252,7 +421,6 @@ impl ReductionResult for ReductionISToVC { &self, target_sol: &Vec, ) -> crate::rules::ExtractionResult> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_sol)?; Ok(target_sol.iter().map(|&x| !x).collect()) } } @@ -260,33 +428,29 @@ impl ReductionResult for ReductionISToVC { ### Solution extraction contract -`ReductionResult::extract_solution` accepts one complete target configuration -and returns the source configuration defined by the reduction. Extraction is a -fallible boundary, not a recovery mechanism: - -1. In every direct extractor, call `validate_target_solution()` once before - indexing or decoding. Composed extractors delegate this check. -2. Validate any structure required by the inverse mapping, such as exactly-one - blocks, permutations, paths, flows, or schedules. -3. Apply the reduction's mathematical inverse once and return a source - configuration with the required length and domains. -4. Return `ExtractionError` when a precondition is not satisfied. +`ReductionResult::extract_solution` maps a complete target solution satisfying +the rule's mathematical premises into a source solution. The adapter establishes +target validity for internal solves. External callers supply witnesses under the +same contract. Rules requiring optimal target solutions document that requirement. +Source YES/NO and optimization outcomes are interpreted by solver orchestration, +not by the extraction chain. Invalid external witnesses have no mapping-correctness +guarantee. -Do not truncate or pad input, substitute zero for missing data, select the -first of several invalid candidates, retry with another mapping, or panic on -caller-provided configuration data. Empty and singleton instances should flow -through the same mathematical mapping unless the reduction itself has a -genuine mathematical case distinction. +Do not repeat checks implied by target constraints or successful construction. +Do not truncate or pad input, substitute values for missing data, retry another +mapping, or add runtime acceptance checks to compensate for a rule defect. +Keep actual mathematical case distinctions and representation errors that can +occur for inputs satisfying the mapping's premises. Zero and sentinel values remain valid when the source model explicitly gives them meaning. For example, `MaximumCommonEdgeSubgraph` includes an "unmapped" sentinel in its source dimensions. Missing target data must never be interpreted as that sentinel. -Each conditional in an extractor should therefore either reject a named -invariant violation or implement a case in the reduction's mathematics. A -normal extractor has one validation phase followed by one decoding phase; it -does not accumulate compatibility or fallback branches. +Each conditional in an extractor should implement a case in the reduction's +mathematics or report an error that remains reachable under its premises. +The external boundary handles parsing and type conversion; extraction does not +accumulate feasibility checks, compatibility branches, or fallbacks. The `#[reduction]` attribute on the `ReduceTo` impl registers the reduction in the global registry (via `inventory`): @@ -409,20 +573,43 @@ proved infeasibility, and `Err` reports an operational failure. | Solver | Description | |--------|-------------| | **BruteForce** | Enumerates a registered finite search space and returns an optimal or satisfying solution. Used for testing and verification. | -| **ILPSolver** | Executes a problem's registered ILP pipeline. Each pipeline terminates at `ILP` or `ILP`, which is solved by HiGHS via `good_lp`. | - -ILP results are optimal or infeasible according to HiGHS numerical tolerances; -zero MIP gaps do not imply mathematical exactness. Integer extraction rounds -variable assignments, validates the original constraints, and recomputes the -source objective with checked integer arithmetic. Floating-point objective -comparisons in numerical regression tests use an explicit acceptance policy -in source units (absolute and relative tolerances of `1e-7` for the QUBO solver -regression), separate from the `1e-6` variable-rounding tolerance. This test -policy is not a universal bound on backend objective error. - -When an ILP target witness misses a source decision threshold, the solver -returns `ILPSolveError::UnresolvedDecision`, not infeasibility: the witness -alone cannot prove that no qualifying source solution exists. +| **ILPSolver** | Executes a problem's registered ILP pipeline. Each pipeline terminates at a native `ILP` with bool/i64 variables and i64/f64 coefficients, solved by the shared HiGHS adapter through its native Rust bindings. | + +### ILP execution boundary + +`ILPSolver::solve

() -> Result` is the typed entry +point. Adapter failures retain their classified errors. Registry lookup, +concrete-terminal dispatch, aggregate interpretation, +and reduction-chain extraction belong to orchestration. Integer pipelines end +at native integer ILPs; they do not need a float-coefficient cast edge to execute. +Explicit coefficient-conversion rules retain their own mathematical contracts. + +The shared internal `HighsAdapter` borrows an `ILP` and returns its existing +`Vec` witness representation. It encodes the backend model, executes it, +interprets termination, checks returned integer values, and validates constraints +and objective arithmetic against the original ILP. It does not inspect source +model names, query reduction registrations, or extract source witnesses. +Unsupported transport produces `InexactTransport`; a rejected returned witness +produces `InvalidSolution`. A validation failure must not relax model constraints. + +Optimality and infeasibility are backend conclusions under HiGHS's numerical +contract, not independent mathematical certificates. An accepted optimum requires +both an optimal backend termination and successful witness validation. Timeouts, +non-optimal termination, and invalid results are errors, not infeasibility. +Variable decoding tolerances belong to the adapter; they do not define source or +target feasibility, nor a universal objective-error allowance for tests. + +After accepting a target optimum, orchestration must apply the reduction's +aggregate mapping to interpret a source decision threshold. If that optimum +cannot meet the threshold, the source answer is NO. A merely feasible witness +or failed solve is insufficient for that conclusion. Typed solving, dynamic +solving, and explicit CLI bundles must share the same interpretation and witness +mapping. + +Fixed pipelines and explicit CLI bundles reuse the executed `ReductionChain` +and the solver completion path. Aggregate mappings interpret an accepted target +optimum before witness extraction. Source evaluation computes requested output +values and propagates evaluation errors; it is not another feasibility gate. ## JSON Serialization @@ -438,3 +625,18 @@ let restored: MaximumIndependentSet = from_json(&json)?; ## Contributing See [Call for Contributions](./open-problems.md) for the recommended issue-based workflow (no coding required). + +### QUBO coefficient storage + +QUBO stores coefficients in `sprs::CsMat` using CSR order. Construction from +linear/quadratic terms preserves last-assignment semantics; reductions accumulate +coefficients with their existing checked arithmetic before compression. Exact +zeros need no stored entry. Evaluation visits the upper triangle in row/column +order, retaining checked integer addition and floating-point summation order. + +`QUBO::from_sparse` accepts a square CSR or CSC matrix; `matrix()` returns the +CSR matrix and `get(i, j)` returns an owned coefficient, including zero for an +unstored in-bounds entry. `from_matrix` and CLI `--matrix` accept dense input. +Persisted QUBO JSON stores the `sprs` matrix object (`storage`, `nrows`, `ncols`, +`indptr`, `indices`, `data`); variable count comes from the matrix dimensions. +Rules, numeric casts, and solver reductions consume sparse coefficients directly. diff --git a/docs/src/static/trait-hierarchy-dark.svg b/docs/src/static/trait-hierarchy-dark.svg index e932783a4..3e35702f4 100644 --- a/docs/src/static/trait-hierarchy-dark.svg +++ b/docs/src/static/trait-hierarchy-dark.svg @@ -1,765 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs/src/static/trait-hierarchy.svg b/docs/src/static/trait-hierarchy.svg index 571ca9c90..a1b82bfb0 100644 --- a/docs/src/static/trait-hierarchy.svg +++ b/docs/src/static/trait-hierarchy.svg @@ -1,765 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs/src/static/trait-hierarchy.typ b/docs/src/static/trait-hierarchy.typ index b6c167ffd..a9ce6343d 100644 --- a/docs/src/static/trait-hierarchy.typ +++ b/docs/src/static/trait-hierarchy.typ @@ -24,14 +24,15 @@ spacing: (8mm, 12mm), // Problem trait (top center) - node((0.6, 0), box(width: 55mm, align(left)[ + node((1, 0), box(width: 55mm, align(left)[ #strong[trait Problem]\ #text(size: 8pt, fill: secondary)[ `const NAME: &str`\ `type Solution`\ `type Value: Clone`\ - `fn size() -> ProblemParameters`\ - `fn evaluate(&solution) -> Value`\ + `fn parameters() -> ProblemParameters`\ + `fn evaluate(&solution)`\ + ` -> Result`\ `fn variant() -> Vec<(&str, &str)>` ] ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), @@ -41,7 +42,8 @@ #strong[trait Aggregate]\ #text(size: 8pt, fill: secondary)[ `fn identity() -> Self`\ - `fn combine(self, other) -> Self`\ + `fn combine(self, other)`\ + ` -> Result`\ `fn is_absorbing(&self) -> bool`\ #strong[trait SolutionAggregate: Aggregate]\ `fn contributes_to_solution(...)` @@ -49,17 +51,19 @@ ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), // Brute-force capability (bottom center) - node((0.7, 1), box(width: 48mm, align(left)[ + node((1, 1), box(width: 48mm, align(left)[ #strong[trait BruteForceProblem]\ #text(size: 8pt, fill: secondary)[ `extends Problem`\ - `fn dimensions() -> Vec`\ + `num_variables()`\ + `dimension(i: usize)`\ + `→ Result`\ #text(style: "italic")[reference solver only] ] ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), // Common value types (bottom right) - node((1.4, 1), box(width: 48mm, align(left)[ + node((0, 2), box(width: 48mm, align(left)[ #strong[Common Value Types]\ #text(size: 8pt, fill: secondary)[ `Max | Min | Extremum`\ diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 0f5cc3834..951a8c84a 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -522,7 +522,7 @@ fn parse_directed_graph( None => inferred_num_v, }; let num_arcs = arcs.len(); - Ok((DirectedGraph::new(num_v, arcs), num_arcs)) + Ok((DirectedGraph::new(num_v, arcs)?, num_arcs)) } /// Parse implication rules from semicolon-separated "antecedents>consequent" strings. diff --git a/problemreductions-cli/src/commands/create/schema_support.rs b/problemreductions-cli/src/commands/create/schema_support.rs index 42d0e5fd5..76cc4d41f 100644 --- a/problemreductions-cli/src/commands/create/schema_support.rs +++ b/problemreductions-cli/src/commands/create/schema_support.rs @@ -1091,7 +1091,7 @@ pub(super) fn parse_simple_graph_value( } None => inferred_num_vertices, }; - SimpleGraph::new(num_vertices, edges) + SimpleGraph::new(num_vertices, edges)? }; Ok(serde_json::to_value(graph)?) } @@ -1154,7 +1154,7 @@ pub(super) fn parse_labelled_digraph_value( arcs.push(LabelledArc::new(src, label, dst)); } } - let graph = LabelledDigraph::new(num_vertices, arcs); + let graph = LabelledDigraph::new(num_vertices, arcs)?; Ok(serde_json::to_value(graph)?) } diff --git a/problemreductions-cli/src/commands/create/tests.rs b/problemreductions-cli/src/commands/create/tests.rs index 79294e200..4d667819a 100644 --- a/problemreductions-cli/src/commands/create/tests.rs +++ b/problemreductions-cli/src/commands/create/tests.rs @@ -1251,7 +1251,7 @@ fn test_create_production_planning_rejects_mismatched_period_lengths() { let err = create(&args, &out).unwrap_err(); assert!(err .to_string() - .contains("demands has 5 entries, expected 6")); + .contains("all per-period vectors must have length num_periods")); } #[test] @@ -2116,7 +2116,7 @@ fn test_create_balanced_complete_bipartite_subgraph_rejects_out_of_range_biedges }; let err = create(&args, &out).unwrap_err().to_string(); - assert!(err.contains("out of bounds for left partition size 4")); + assert!(err.contains("left vertex 4 out of bounds")); } #[test] diff --git a/problemreductions-cli/src/commands/evaluate.rs b/problemreductions-cli/src/commands/evaluate.rs index 7ce07cadf..32d379780 100644 --- a/problemreductions-cli/src/commands/evaluate.rs +++ b/problemreductions-cli/src/commands/evaluate.rs @@ -28,7 +28,7 @@ pub fn evaluate(input: &Path, config_str: &str, out: &OutputConfig) -> Result<() let config: serde_json::Value = serde_json::from_str(config_str).context("Config is not valid JSON")?; - let result = problem.evaluate_dyn(&config)?; + let (result, _) = problem.evaluate_dyn(&config)?; out.emit( || result.to_string(), diff --git a/problemreductions-cli/src/commands/extract.rs b/problemreductions-cli/src/commands/extract.rs index 79736a984..4b6a60db2 100644 --- a/problemreductions-cli/src/commands/extract.rs +++ b/problemreductions-cli/src/commands/extract.rs @@ -30,9 +30,7 @@ pub fn extract(input: &Path, config_str: &str, out: &OutputConfig) -> Result<()> let replay = BundleReplay::prepare(&bundle)?; - let target_eval = replay.target.evaluate_dyn(&target_config)?; - - let (source_config, source_eval) = replay.extract(&target_config)?; + let (source_config, source_eval, target_eval) = replay.extract(&target_config)?; out.emit( || { diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 341125bb4..b0e1e2980 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -42,7 +42,7 @@ impl LoadedProblem { pub fn brute_force_num_variables(&self) -> Result> { brute_force_dimensions(&self.inner) .map(|dimensions| dimensions.map(|dimensions| dimensions.len())) - .map_err(|error| anyhow::anyhow!("solver capability registry is invalid: {error}")) + .map_err(|error| anyhow::anyhow!("cannot inspect brute-force coordinates: {error}")) } pub fn solve(&self, request: SolverRequest) -> Result { @@ -77,7 +77,7 @@ pub struct SolverCapabilitiesView { pub fn solver_capabilities_view(problem: &LoadedProblem) -> Result { let key = ExactProblemKey::new(problem.problem_name(), problem.variant_map()); let registered = solver_capabilities(&key) - .map_err(|error| anyhow::anyhow!("solver capability registry is invalid: {error}"))?; + .map_err(|error| anyhow::anyhow!("cannot inspect brute-force coordinates: {error}"))?; let customized = registered .customized .map(|entry| CustomizedSolverCapabilityView { @@ -292,19 +292,15 @@ impl BundleReplay { }) } - /// Map a target-space configuration back to the source space and evaluate it. + /// Map a target witness under the reduction contract and evaluate for display. pub fn extract( &self, target_config: &serde_json::Value, - ) -> Result<(serde_json::Value, String)> { + ) -> Result<(serde_json::Value, String, String)> { + let (target_eval, _) = self.target.evaluate_dyn(target_config)?; let source_config = self.chain.extract_solution_json(target_config.clone())?; - let source_eval = self.source.evaluate_witness_dyn(&source_config)?.ok_or_else(|| { - problemreductions::rules::ExtractionError::invalid(format!( - "extracted solution is infeasible for {}; the reduction did not establish a source solution", - self.source_name - )) - })?; - Ok((source_config, source_eval)) + let (source_eval, _) = self.source.evaluate_dyn(&source_config)?; + Ok((source_config, source_eval, target_eval)) } /// Solve the target and map the result back to the source problem. @@ -312,25 +308,12 @@ impl BundleReplay { pub(crate) fn solve(&self, request: SolverRequest) -> Result { let target_result = self.target.solve(request)?; let solver = target_result.solver; - let (source_outcome, target_outcome) = match target_result.outcome { - SolveOutcome::Optimal { - solution: target_solution, - evaluation: target_evaluation, - } => { - let (source_solution, source_evaluation) = self.extract(&target_solution)?; - ( - SolveOutcome::Optimal { - solution: source_solution, - evaluation: source_evaluation, - }, - SolveOutcome::Optimal { - solution: target_solution, - evaluation: target_evaluation, - }, - ) - } - SolveOutcome::Infeasible => (SolveOutcome::Infeasible, SolveOutcome::Infeasible), - }; + let target_outcome = target_result.outcome; + let source_outcome = problemreductions::solvers::complete_reduction( + &*self.source, + &self.chain, + &target_outcome, + )?; Ok(BundleSolveResult { source_name: self.source_name.clone(), @@ -428,7 +411,59 @@ mod tests { use serde_json::json; #[test] - fn bundle_rejects_infeasible_extracted_witness() { + fn ilp_qubo_bundle_maps_optima_and_infeasibility() { + use problemreductions::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; + use problemreductions::Problem; + + for rhs in [1, -1] { + let ilp = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, 1), (1, 1)], rhs)], + vec![(0, 3), (1, 2)], + ObjectiveSense::Maximize, + ) + .unwrap(); + let source = ProblemJson { + problem_type: "ILP".into(), + variant: ILP::::variant() + .into_iter() + .map(|(key, value)| (key.to_string(), value.to_string())) + .collect(), + data: serde_json::to_value(&ilp).unwrap(), + }; + let route = crate::commands::reduce::parse_path_json( + r#"{"path":[{"from":{"name":"ILP","variant":{"variable":"bool","coefficient":"i64"}},"to":{"name":"QUBO","variant":{"weight":"i64"}}}]}"#, + ).unwrap(); + let bundle = crate::commands::reduce::execute_route(source, route).unwrap(); + let replay = BundleReplay::prepare(&bundle).unwrap(); + for backend in [SolverRequest::BruteForce, SolverRequest::Ilp] { + let result = replay.solve(backend).unwrap(); + if rhs == 1 { + let SolveOutcome::Optimal { solution, .. } = result.source_outcome else { + panic!("the ILP has an optimum"); + }; + assert_eq!(solution, json!([1, 0])); + let SolveOutcome::Optimal { + solution: target, .. + } = result.target_outcome + else { + panic!("the QUBO has an optimum"); + }; + assert_eq!(target, json!([true, false, false])); + assert_eq!(replay.extract(&target).unwrap().0, solution); + } else { + assert_eq!(result.source_outcome, SolveOutcome::Infeasible); + assert!(matches!( + result.target_outcome, + SolveOutcome::Optimal { .. } + )); + } + } + } + } + + #[test] + fn bundle_maps_satisfiability_outcomes_through_the_value_relation() { for (clauses, feasible) in [ (vec![vec![1, 1, 1], vec![-1, -1, -1]], false), (vec![vec![1, 1, 1], vec![1, 1, 1]], true), @@ -460,13 +495,61 @@ mod tests { assert!(matches!(result.unwrap().source_outcome, SolveOutcome::Optimal { evaluation, .. } if evaluation == "Or(true)")); } else { - let error = result.err().unwrap(); - assert!(error - .downcast_ref::() - .is_some()); - assert!(error - .to_string() - .contains("extracted solution is infeasible")); + assert!(matches!( + result.unwrap().source_outcome, + SolveOutcome::Infeasible + )); + } + } + } + + #[test] + fn bundle_and_registered_pipeline_agree_on_decision_thresholds() { + for bound in [0, 1] { + let source = ProblemJson { + problem_type: "DecisionMinimumVertexCover".into(), + variant: BTreeMap::from([ + ("graph".into(), "SimpleGraph".into()), + ("weight".into(), "i64".into()), + ]), + data: json!({ + "inner": {"graph": {"num_vertices": 2, "edges": [[0,1]]}, "weights": [1,1]}, + "bound": bound, + }), + }; + let route = crate::commands::reduce::parse_path_json( + r#"{"path":[{ + "from":{"name":"DecisionMinimumVertexCover","variant":{"graph":"SimpleGraph","weight":"i64"}}, + "to":{"name":"MinimumVertexCover","variant":{"graph":"SimpleGraph","weight":"i64"}} + }]}"#, + ).unwrap(); + let bundle = crate::commands::reduce::execute_route(source, route).unwrap(); + let replay = BundleReplay::prepare(&bundle).unwrap(); + if bound == 1 { + assert_eq!( + replay.extract(&json!([true, false])).unwrap().0, + json!([true, false]) + ); + } + for backend in [ + SolverRequest::BruteForce, + SolverRequest::Ilp, + SolverRequest::Default, + ] { + let result = replay.solve(backend).unwrap(); + assert!(matches!( + result.target_outcome, + SolveOutcome::Optimal { .. } + )); + assert_eq!( + matches!(result.source_outcome, SolveOutcome::Infeasible), + bound == 0 + ); + let direct = replay.source.solve(backend).unwrap(); + assert_eq!( + matches!(direct.outcome, SolveOutcome::Infeasible), + bound == 0 + ); } } } @@ -511,7 +594,9 @@ mod tests { #[test] fn test_load_problem_alias_uses_registry_dispatch() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); let variant = BTreeMap::from([ ("graph".to_string(), "SimpleGraph".to_string()), ("weight".to_string(), "i64".to_string()), @@ -596,7 +681,7 @@ mod tests { ); let err = loaded.err().unwrap(); assert!( - err.to_string().contains("expected positive integer, got 0"), + err.to_string().contains("num_processors must be positive"), "unexpected error: {err}" ); } @@ -699,7 +784,7 @@ mod tests { use problemreductions::models::graph::RootedTreeArrangement; use problemreductions::Problem; - let problem = RootedTreeArrangement::new(SimpleGraph::new(2, vec![(0, 1)]), 1); + let problem = RootedTreeArrangement::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 1); let loaded = load_problem( RootedTreeArrangement::::NAME, &BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]), diff --git a/problemreductions-cli/src/mcp/tools.rs b/problemreductions-cli/src/mcp/tools.rs index 09cddd892..149d7398c 100644 --- a/problemreductions-cli/src/mcp/tools.rs +++ b/problemreductions-cli/src/mcp/tools.rs @@ -414,7 +414,7 @@ impl McpServer { let pj: ProblemJson = serde_json::from_str(problem_json)?; let problem = load_problem(&pj.problem_type, &pj.variant, pj.data)?; - let result = problem.evaluate_dyn(config)?; + let (result, _) = problem.evaluate_dyn(config)?; let json = serde_json::json!({ "problem": problem.problem_name(), "config": config, diff --git a/problemreductions-cli/src/test_support.rs b/problemreductions-cli/src/test_support.rs index ba3d7e15d..a63f94154 100644 --- a/problemreductions-cli/src/test_support.rs +++ b/problemreductions-cli/src/test_support.rs @@ -5,8 +5,9 @@ use problemreductions::registry::{ }; use problemreductions::rules::registry::{ReductionEntry, ReductionParameterDeclarations}; use problemreductions::rules::{AggregateReductionResult, VariantReductionResult}; +use problemreductions::solvers::SolutionAggregate; use problemreductions::traits::Problem; -use problemreductions::types::{Aggregate, Extremum, Max, SolutionAggregate}; +use problemreductions::types::{Aggregate, Extremum, Max}; use serde::{Deserialize, Serialize}; use std::any::Any; use std::collections::BTreeMap; @@ -67,8 +68,12 @@ impl Problem for AggregateValueSource { } impl problemreductions::solvers::BruteForceProblem for AggregateValueSource { - fn dimensions(&self) -> Vec { - vec![2; self.values.len()] + fn num_variables(&self) -> Result { + Ok(self.values.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -109,8 +114,12 @@ impl Problem for AggregateValueTarget { } impl problemreductions::solvers::BruteForceProblem for AggregateValueTarget { - fn dimensions(&self) -> Vec { - vec![2] + fn num_variables(&self) -> Result { + Ok(1usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([2][variable]) } } @@ -139,24 +148,23 @@ fn decode_bits(indices: Vec) -> Vec { fn cartesian_indices( dimensions: Vec, ) -> Result>, problemreductions::solvers::SolveError> { - let total = if dimensions.is_empty() { - 1 - } else if dimensions.contains(&0) { - 0 + let mut current = if dimensions.contains(&0) { + None } else { - dimensions.iter().try_fold(1usize, |total, &dimension| { - total.checked_mul(dimension).ok_or_else(|| { - problemreductions::solvers::SolveError::SearchSpaceOverflow(dimensions.clone()) - }) - })? + Some(vec![0; dimensions.len()]) }; - Ok((0..total).map(move |mut index| { - let mut coordinates = vec![0; dimensions.len()]; + Ok(std::iter::from_fn(move || { + let result = current.take()?; + let mut next = result.clone(); for position in (0..dimensions.len()).rev() { - coordinates[position] = index % dimensions[position]; - index /= dimensions[position]; + next[position] += 1; + if next[position] < dimensions[position] { + current = Some(next); + break; + } + next[position] = 0; } - coordinates + Some(result) })) } @@ -166,7 +174,7 @@ where P::Value: Aggregate, { let mut total = P::Value::identity(); - for indices in cartesian_indices(problem.dimensions())? { + for indices in cartesian_indices(problemreductions::solvers::cartesian_dimensions(problem)?)? { total = total.combine(problem.evaluate(&decode_bits(indices))?)?; } Ok(total) @@ -180,7 +188,7 @@ where P::Value: SolutionAggregate, { let total = solve_cartesian(problem)?; - for indices in cartesian_indices(problem.dimensions())? { + for indices in cartesian_indices(problemreductions::solvers::cartesian_dimensions(problem)?)? { let solution = decode_bits(indices); let value = problem.evaluate(&solution)?; if P::Value::contributes_to_solution(&value, &total) { @@ -199,7 +207,7 @@ where { let total = solve_cartesian(problem)?; let mut witnesses = Vec::new(); - for indices in cartesian_indices(problem.dimensions())? { + for indices in cartesian_indices(problemreductions::solvers::cartesian_dimensions(problem)?)? { let solution = decode_bits(indices); let value = problem.evaluate(&solution)?; if P::Value::contributes_to_solution(&value, &total) { @@ -331,7 +339,7 @@ problemreductions::inventory::submit! { let problem = any .downcast_ref::() .expect("AggregateValueSource brute-force dimensions type mismatch"); - problemreductions::solvers::BruteForceProblem::dimensions(problem) + problemreductions::solvers::cartesian_dimensions(problem) }, solve_fn: solve_dynamic::, solve_typed_fn: solve_typed::, @@ -379,7 +387,7 @@ problemreductions::inventory::submit! { let problem = any .downcast_ref::() .expect("AggregateValueTarget brute-force dimensions type mismatch"); - problemreductions::solvers::BruteForceProblem::dimensions(problem) + problemreductions::solvers::cartesian_dimensions(problem) }, solve_fn: solve_dynamic::, solve_typed_fn: solve_typed::, @@ -490,3 +498,6 @@ pub(crate) fn aggregate_bundle() -> ReductionBundle { ], } } + +problemreductions::impl_dyn_problem!(AggregateValueSource); +problemreductions::impl_dyn_problem!(AggregateValueTarget); diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 90224cc29..47ff0d6f9 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -971,7 +971,7 @@ fn test_create_undirected_two_commodity_integral_flow_rejects_wrong_capacity_cou .unwrap(); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("capacities length must match graph edge count")); + assert!(stderr.contains("capacities length must match graph num_edges")); assert!(stderr.contains("Usage: pred create UndirectedTwoCommodityIntegralFlow")); } @@ -1168,7 +1168,7 @@ fn test_create_integral_flow_bundles_rejects_out_of_range_bundle_arc() { .unwrap(); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("bundle 1 arc is out of range")); + assert!(stderr.contains("bundle 1 references arc")); assert!(stderr.contains("Usage: pred create IntegralFlowBundles")); assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); } @@ -1373,7 +1373,7 @@ fn test_create_integral_flow_with_multipliers_rejects_wrong_multiplier_count() { .unwrap(); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("multipliers length must match num_vertices")); + assert!(stderr.contains("multipliers length must match graph num_vertices")); assert!(stderr.contains("Usage: pred create IntegralFlowWithMultipliers")); } @@ -3810,7 +3810,10 @@ fn test_create_bounded_component_spanning_forest_rejects_zero_k() { .unwrap(); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("k must be at least 1"), "stderr: {stderr}"); + assert!( + stderr.contains("max_components must be at least 1"), + "stderr: {stderr}" + ); } #[test] @@ -4729,21 +4732,18 @@ fn test_create_shortest_common_supersequence_derives_internal_fields() { } #[test] -fn test_create_lcs_rejects_empty_strings_without_panicking() { +fn test_create_lcs_accepts_empty_strings() { let output = pred() .args(["create", "LCS", "--strings", ""]) .output() .unwrap(); - assert!(!output.status.success()); - let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("at least one input string must be non-empty"), - "expected user-facing validation error, got: {stderr}" - ); - assert!( - !stderr.contains("panicked at"), - "create command should reject invalid LCS input without panicking: {stderr}" + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) ); + let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(json["data"]["max_length"], 0); } #[test] @@ -7428,7 +7428,7 @@ fn test_create_bcnf_rejects_out_of_range_attribute_indices() { "CLI should return a user-facing error, got: {stderr}" ); assert!( - stderr.contains("outside universe of size 3"), + stderr.contains("out of range (num_attributes = 3)"), "expected out-of-range error, got: {stderr}" ); } @@ -7454,8 +7454,8 @@ fn test_create_bcnf_rejects_out_of_range_lhs_attribute_indices() { ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("subsets[0] contains attribute 4 outside universe of size 3"), - "expected lhs-specific out-of-range error, got: {stderr}" + stderr.contains("Functional dependency 0 contains attribute 4"), + "expected dependency out-of-range error, got: {stderr}" ); } @@ -7480,7 +7480,7 @@ fn test_create_bcnf_rejects_out_of_range_target_attribute_indices() { ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("target contains attribute 4 outside universe of size 3"), + stderr.contains("target_subset contains attribute 4"), "expected target-specific out-of-range error, got: {stderr}" ); } @@ -7845,7 +7845,7 @@ fn test_evaluate_multiprocessor_scheduling_rejects_zero_processors_json() { ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("expected positive integer, got 0"), + stderr.contains("num_processors must be positive"), "stderr: {stderr}" ); @@ -8632,7 +8632,7 @@ fn test_create_shortest_weight_constrained_path_edge_length_count_mismatch() { assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("edge_lengths has 7 entries, expected 8"), + stderr.contains("edge lengths length must match num_edges"), "stderr: {stderr}" ); } @@ -8678,7 +8678,7 @@ fn test_create_shortest_weight_constrained_path_rejects_out_of_bounds_source_ver assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("source_vertex 9 is outside graph with 6 vertices"), + stderr.contains("source_vertex 9 out of bounds (graph has 6 vertices)"), "stderr: {stderr}" ); assert!( @@ -8766,7 +8766,7 @@ fn test_create_shortest_weight_constrained_path_rejects_non_positive_edge_length assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("edge_lengths must be positive"), + stderr.contains("edge lengths must be positive"), "stderr: {stderr}" ); } @@ -9648,7 +9648,7 @@ fn test_extract_roundtrip_mis_to_qubo() { } #[test] -fn test_extract_rejects_structurally_invalid_one_hot_config() { +fn test_extract_decodes_a_qualifying_tour() { let problem_file = std::env::temp_dir().join("pred_test_extract_tsp_in.json"); let bundle_file = std::env::temp_dir().join("pred_test_extract_tsp_bundle.json"); @@ -9686,19 +9686,22 @@ fn test_extract_rejects_structurally_invalid_one_hot_config() { let extract_out = pred() .args([ + "--json", "extract", bundle_file.to_str().unwrap(), "--config", - "[false,false,false,false,false,false,false,false,false]", + "[true,false,false,false,true,false,false,false,true]", ]) .output() .unwrap(); - assert!(!extract_out.status.success()); - let stderr = String::from_utf8(extract_out.stderr).unwrap(); assert!( - stderr.contains("tour position 0 does not select exactly one vertex"), - "unexpected stderr: {stderr}" + extract_out.status.success(), + "{}", + String::from_utf8_lossy(&extract_out.stderr) ); + let json: serde_json::Value = serde_json::from_slice(&extract_out.stdout).unwrap(); + assert_eq!(json["solution"], serde_json::json!([true, true, true])); + assert_eq!(json["evaluation"], "Min(3)"); std::fs::remove_file(&problem_file).ok(); std::fs::remove_file(&bundle_file).ok(); @@ -9950,7 +9953,7 @@ fn test_extract_rejects_tampered_target_data() { // what the reduction chain actually produces. let bundle_text = std::fs::read_to_string(&bundle_file).unwrap(); let mut bundle: serde_json::Value = serde_json::from_str(&bundle_text).unwrap(); - bundle["target"]["data"]["matrix"][0][0] = serde_json::json!(999.0); + bundle["target"]["data"]["matrix"]["data"][0] = serde_json::json!(999.0); let mut f = std::fs::File::create(&tampered_file).unwrap(); f.write_all(bundle.to_string().as_bytes()).unwrap(); diff --git a/problemreductions-macros/src/lib.rs b/problemreductions-macros/src/lib.rs index 9ee54082e..a50843782 100644 --- a/problemreductions-macros/src/lib.rs +++ b/problemreductions-macros/src/lib.rs @@ -531,6 +531,30 @@ fn generate_reduction_entry( quote! { None } }; + let aggregate_view = if attrs.aggregate { + quote! { Some(result.clone()) } + } else { + quote! { None } + }; + + let interpret_optimum = if attrs.aggregate { + quote! { + Some({ + let result = result.clone(); + std::rc::Rc::new(move |solution: &dyn std::any::Any| { + let solution = solution.downcast_ref::<<#target_type as crate::traits::Problem>::Solution>() + .ok_or_else(|| crate::rules::ExtractionError::invalid("target solution type mismatch"))?; + let target = crate::rules::ReductionResult::target_problem(result.as_ref()); + let value = crate::traits::Problem::evaluate(target, solution)?; + let value = crate::rules::AggregateReductionResult::extract_value(result.as_ref(), value); + Ok(value.is_valid()) + }) + }) + } + } else { + quote! { None } + }; + // Collect generic parameter info from the impl block let type_generics = collect_type_generic_names(&impl_block.generics); @@ -572,12 +596,17 @@ fn generate_reduction_entry( unavailable: vec![#(#unavailable_tokens),*], }, module_path: module_path!(), - reduce_fn: Some(|src: &dyn std::any::Any| -> Result, crate::rules::ReductionError> { + reduce_fn: Some(|src: &dyn std::any::Any| -> Result { let src = src.downcast_ref::<#source_type>().ok_or_else( crate::rules::ReductionError::source_type_mismatch::<#source_type, #target_type>, )?; let result = <#source_type as crate::rules::ReduceTo<#target_type>>::reduce_to(src)?; - Ok(Box::new(result)) + let result = std::rc::Rc::new(result); + Ok(crate::rules::registry::ExecutedStep { + aggregate: #aggregate_view, + interpret_optimum: #interpret_optimum, + witness: result, + }) }), reduce_aggregate_fn: #reduce_aggregate_fn, turing: false, @@ -787,7 +816,7 @@ pub fn register_brute_force(input: TokenStream) -> TokenStream { let problem = any .downcast_ref::<#ty>() .expect("brute-force registration received the wrong problem type"); - <#ty as crate::solvers::BruteForceProblem>::dimensions(problem) + crate::solvers::cartesian_dimensions(problem) }, solve_fn: |any| { let problem = any @@ -944,6 +973,7 @@ fn generate_declare_variants(input: &DeclareVariantsInput) -> syn::Result(source: S) -> RuleExample where S: Problem + Serialize + ReduceTo>, V: crate::models::algebraic::VariableDomain, - C: crate::models::algebraic::ILPCoefficient + Serialize, + C: crate::models::algebraic::ILPCoefficient + Serialize + serde::de::DeserializeOwned, >>::Result: ReductionResult>, S::Solution: Serialize, diff --git a/src/io.rs b/src/io.rs index 9e3eb8dc6..2b5c4f0d3 100644 --- a/src/io.rs +++ b/src/io.rs @@ -44,7 +44,7 @@ impl FileFormat { /// use problemreductions::models::graph::MaximumIndependentSet; /// use problemreductions::topology::SimpleGraph; /// -/// let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); +/// let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1i64; 3]).unwrap(); /// write_problem(&problem, "problem.json", FileFormat::Json).unwrap(); /// ``` pub fn write_problem>( diff --git a/src/lib.rs b/src/lib.rs index 69381f493..ae4edab0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,8 +75,8 @@ pub mod prelude { MultipleChoiceBranching, MultipleCopyFileAllocation, OptimalLinearArrangement, PartialFeedbackEdgeSet, PartitionIntoCliques, PartitionIntoPathsOfLength2, PartitionIntoTriangles, PathConstrainedNetworkFlow, RootedTreeArrangement, RuralPostman, - ShortestWeightConstrainedPath, SteinerTreeInGraphs, TravelingSalesman, - UndirectedFlowLowerBounds, UndirectedTwoCommodityIntegralFlow, + ShortestWeightConstrainedPath, TravelingSalesman, UndirectedFlowLowerBounds, + UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, CbqRelation, diff --git a/src/models/algebraic/algebraic_equations_over_gf2.rs b/src/models/algebraic/algebraic_equations_over_gf2.rs index 32649b5f2..57d09ef2d 100644 --- a/src/models/algebraic/algebraic_equations_over_gf2.rs +++ b/src/models/algebraic/algebraic_equations_over_gf2.rs @@ -204,8 +204,12 @@ impl Problem for AlgebraicEquationsOverGF2 { } impl crate::solvers::BruteForceProblem for AlgebraicEquationsOverGF2 { - fn dimensions(&self) -> Vec { - vec![2; self.num_variables] + fn num_variables(&self) -> Result { + Ok(self.num_variables) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/algebraic/bmf.rs b/src/models/algebraic/bmf.rs index 52aeb23e8..3db73f7ab 100644 --- a/src/models/algebraic/bmf.rs +++ b/src/models/algebraic/bmf.rs @@ -47,13 +47,14 @@ inventory::submit! { /// vec![true, false], /// vec![false, true], /// ]; -/// let problem = BMF::new(a, 2); +/// let problem = BMF::new(a, 2).unwrap(); /// /// let solver = BruteForce::new(); /// let witness = solver.solve(&problem).unwrap().unwrap(); /// assert!(problem.is_exact(&witness).unwrap()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "BMFData")] pub struct BMF { /// The target matrix A (m x n). matrix: Vec>, @@ -65,22 +66,39 @@ pub struct BMF { k: usize, } +#[derive(Deserialize)] +struct BMFData { + matrix: Vec>, + k: usize, +} + +impl TryFrom for BMF { + type Error = crate::registry::ConstructionError; + + fn try_from(data: BMFData) -> Result { + Self::new(data.matrix, data.k) + } +} + impl BMF { /// Create a new BMF problem. /// /// # Arguments /// * `matrix` - The target m x n boolean matrix /// * `k` - The factorization rank - pub fn new(matrix: Vec>, k: usize) -> Self { + /// # Errors + /// + /// Returns an error when matrix dimensions violate the instance definition. + pub fn new( + matrix: Vec>, + k: usize, + ) -> Result { let m = matrix.len(); - let n = if m > 0 { matrix[0].len() } else { 0 }; - - // Validate matrix dimensions - for row in &matrix { - assert_eq!(row.len(), n, "All rows must have the same length"); + let n = matrix.first().map_or(0, Vec::len); + if matrix.iter().any(|row| row.len() != n) { + return Err("all matrix rows must have the same length".into()); } - - Self { matrix, m, n, k } + Ok(Self { matrix, m, n, k }) } /// Get the number of rows. @@ -247,9 +265,20 @@ impl Problem for BMF { } impl crate::solvers::BruteForceProblem for BMF { - fn dimensions(&self) -> Vec { - // B: m*k + C: k*n binary variables - vec![2; self.m * self.k + self.k * self.n] + fn num_variables(&self) -> Result { + ((self.m).checked_mul(self.k).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + })?) + .checked_add((self.k).checked_mul(self.n).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + })?) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -271,14 +300,17 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "bmf", - instance: Box::new(BMF::new( - vec![ - vec![true, true, false], - vec![true, true, true], - vec![false, true, true], - ], - 2, - )), + instance: Box::new( + BMF::new( + vec![ + vec![true, true, false], + vec![true, true, true], + vec![false, true, true], + ], + 2, + ) + .unwrap(), + ), // B = [[1,0],[1,1],[0,1]], C = [[1,1,0],[0,1,1]]. // Total 1s: 4 in B + 4 in C = 8, and B * C = A exactly. optimal_config: serde_json::json!(( diff --git a/src/models/algebraic/closest_vector_problem.rs b/src/models/algebraic/closest_vector_problem.rs index d166b32fb..d2ab02c83 100644 --- a/src/models/algebraic/closest_vector_problem.rs +++ b/src/models/algebraic/closest_vector_problem.rs @@ -1,11 +1,14 @@ //! Closest Vector Problem (CVP). //! //! Given an integer lattice basis `B` and a target vector `t`, find integer -//! coefficients `x` minimizing `||Bx - t||_2`. +//! coefficients `x` minimizing the squared distance `||Bx - t||_2^2`. use crate::registry::{ConstructionError, CreateSpec, ProblemSchemaEntry, VariantDimension}; use crate::traits::{EvaluationError, Problem}; use crate::types::Min; +use num_bigint::BigInt; +use num_rational::BigRational; +use num_traits::Zero; use serde::{Deserialize, Serialize}; /// Target coordinate domains supported by [`ClosestVectorProblem`]. @@ -16,8 +19,8 @@ pub trait ClosestVectorTarget: Clone + std::fmt::Debug + 'static { /// Validate one stored target coordinate. fn validate(&self, index: usize) -> Result<(), ConstructionError>; - /// Convert one coordinate for numerical evaluation and solving. - fn to_f64(&self) -> Result; + /// Represent a stored coordinate exactly for distance evaluation and solving. + fn to_rational(&self) -> BigRational; } impl ClosestVectorTarget for i64 { @@ -27,9 +30,8 @@ impl ClosestVectorTarget for i64 { Ok(()) } - fn to_f64(&self) -> Result { - crate::types::i64_to_exact_f64(*self) - .map_err(|error| EvaluationError::InexactFloatConversion(error.to_string())) + fn to_rational(&self) -> BigRational { + BigRational::from_integer((*self).into()) } } @@ -46,8 +48,8 @@ impl ClosestVectorTarget for f64 { } } - fn to_f64(&self) -> Result { - Ok(*self) + fn to_rational(&self) -> BigRational { + BigRational::from_float(*self).expect("CVP target coordinate must be finite") } } @@ -119,7 +121,7 @@ impl ClosestVectorProblem { basis.len() ))); } - if independent_rows(&basis, ambient_dimension)?.is_none() { + if independent_rows(&basis, ambient_dimension).is_none() { return Err(ConstructionError::Conversion( "closest-vector basis columns must be linearly independent".into(), )); @@ -147,62 +149,72 @@ impl ClosestVectorProblem { &self.target } - pub(crate) fn independent_rows(&self) -> Result, ConstructionError> { - independent_rows(&self.basis, self.ambient_dimension())?.ok_or_else(|| { - ConstructionError::Conversion( - "closest-vector basis columns must be linearly independent".into(), - ) - }) + pub(crate) fn independent_rows(&self) -> Vec { + independent_rows(&self.basis, self.ambient_dimension()) + .expect("CVP basis columns must be independent") + } + + /// Exact squared distance from the lattice point to the stored target. + pub fn squared_distance(&self, solution: &[i64]) -> Result { + if solution.len() != self.num_basis_vectors() { + return Err(EvaluationError::InvalidConfiguration(format!( + "expected {} closest-vector coefficients, got {}", + self.num_basis_vectors(), + solution.len() + ))); + } + Ok(self + .target + .iter() + .enumerate() + .map(|(row, target)| { + let coordinate: BigInt = solution + .iter() + .zip(&self.basis) + .map(|(&coefficient, column)| BigInt::from(coefficient) * column[row]) + .sum(); + let difference = BigRational::from_integer(coordinate) - target.to_rational(); + &difference * &difference + }) + .sum()) } } -fn independent_rows( - basis: &[Vec], - ambient_dimension: usize, -) -> Result>, ConstructionError> { +fn independent_rows(basis: &[Vec], ambient_dimension: usize) -> Option> { let num_columns = basis.len(); if num_columns == 0 { - return Ok(Some(Vec::new())); + return Some(Vec::new()); } let mut matrix = (0..ambient_dimension) - .map(|row| basis.iter().map(|column| column[row]).collect::>()) + .map(|row| { + basis + .iter() + .map(|column| BigInt::from(column[row])) + .collect::>() + }) .collect::>(); - let mut previous_pivot = 1_i64; + let mut previous_pivot = BigInt::from(1); let mut row_indices = (0..ambient_dimension).collect::>(); for column in 0..num_columns { - let Some(pivot_row) = (column..ambient_dimension).find(|&row| matrix[row][column] != 0) - else { - return Ok(None); - }; + let pivot_row = (column..ambient_dimension).find(|&row| !matrix[row][column].is_zero())?; matrix.swap(column, pivot_row); row_indices.swap(column, pivot_row); - let pivot = matrix[column][column]; + let pivot = matrix[column][column].clone(); for row in (column + 1)..ambient_dimension { for next_column in (column + 1)..num_columns { - let left = matrix[row][next_column] - .checked_mul(pivot) - .ok_or_else(rank_overflow)?; - let right = matrix[row][column] - .checked_mul(matrix[column][next_column]) - .ok_or_else(rank_overflow)?; - let numerator = left.checked_sub(right).ok_or_else(rank_overflow)?; - matrix[row][next_column] = numerator - .checked_div(previous_pivot) - .ok_or_else(rank_overflow)?; + matrix[row][next_column] = (&matrix[row][next_column] * &pivot + - &matrix[row][column] * &matrix[column][next_column]) + / &previous_pivot; } - matrix[row][column] = 0; + matrix[row][column] = BigInt::zero(); } previous_pivot = pivot; } row_indices.truncate(num_columns); - Ok(Some(row_indices)) -} - -fn rank_overflow() -> ConstructionError { - ConstructionError::IntegerOverflow("checking closest-vector basis rank".into()) + Some(row_indices) } impl<'de, T> Deserialize<'de> for ClosestVectorProblem @@ -230,58 +242,15 @@ where { const NAME: &'static str = "ClosestVectorProblem"; type Solution = Vec; - type Value = Min; + type Value = Min; crate::problem_parameters![ ("ambient_dimension", ambient_dimension), ("num_basis_vectors", num_basis_vectors), ]; - fn evaluate(&self, solution: &Self::Solution) -> Result, EvaluationError> { - if solution.len() != self.num_basis_vectors() { - return Err(EvaluationError::InvalidConfiguration(format!( - "expected {} closest-vector coefficients, got {}", - self.num_basis_vectors(), - solution.len() - ))); - } - - let mut displacement = self - .target - .iter() - .map(ClosestVectorTarget::to_f64) - .collect::, _>>()?; - for value in &mut displacement { - *value = -*value; - } - - for (&coefficient, column) in solution.iter().zip(&self.basis) { - let coefficient = crate::types::i64_to_exact_f64(coefficient) - .map_err(|error| EvaluationError::InexactFloatConversion(error.to_string()))?; - for (value, &basis_entry) in displacement.iter_mut().zip(column) { - let basis_entry = crate::types::i64_to_exact_f64(basis_entry) - .map_err(|error| EvaluationError::InexactFloatConversion(error.to_string()))?; - let next = *value + coefficient * basis_entry; - if !next.is_finite() { - return Err(EvaluationError::NonFiniteResult( - "computing closest-vector displacement".into(), - )); - } - *value = next; - } - } - - let squared_norm = displacement.into_iter().try_fold(0.0, |total, value| { - let next = total + value * value; - if next.is_finite() { - Ok(next) - } else { - Err(EvaluationError::NonFiniteResult( - "computing closest-vector norm".into(), - )) - } - })?; - Ok(Min(Some(squared_norm.sqrt()))) + fn evaluate(&self, solution: &Self::Solution) -> Result { + Ok(Min(Some(self.squared_distance(solution)?))) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -303,7 +272,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec for ConsecutiveBlockMinimiz type Error = crate::registry::ConstructionError; fn try_from(spec: ConsecutiveBlockMinimizationCreateSpec) -> Result { - Self::try_new(spec.matrix, spec.bound_k) + Self::new(spec.matrix, spec.bound_k) } } @@ -94,15 +94,9 @@ impl ConsecutiveBlockMinimization { /// * `matrix` - The m x n binary matrix /// * `bound` - Upper bound on total consecutive blocks /// - /// # Panics - /// Panics if rows have inconsistent lengths. - pub fn new(matrix: Vec>, bound: i64) -> Self { - Self::try_new(matrix, bound).unwrap_or_else(|err| panic!("{err}")) - } - - /// Create a new ConsecutiveBlockMinimization problem, returning an error - /// instead of panicking when the matrix is ragged. - pub fn try_new( + /// # Errors + /// Returns an error if rows have inconsistent lengths. + pub fn new( matrix: Vec>, bound: i64, ) -> Result { @@ -222,8 +216,12 @@ impl Problem for ConsecutiveBlockMinimization { } impl crate::solvers::BruteForceProblem for ConsecutiveBlockMinimization { - fn dimensions(&self) -> Vec { - vec![self.num_cols; self.num_cols] + fn num_variables(&self) -> Result { + Ok(self.num_cols) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_cols) } } @@ -245,7 +243,7 @@ impl TryFrom for ConsecutiveBlockMinimization { type Error = crate::registry::ConstructionError; fn try_from(value: ConsecutiveBlockMinimizationDef) -> Result { - Self::try_new(value.matrix, value.bound) + Self::new(value.matrix, value.bound) } } @@ -279,17 +277,20 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec { - vec![self.num_cols(); self.num_cols()] + fn num_variables(&self) -> Result { + Ok(self.num_cols()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_cols()) } } diff --git a/src/models/algebraic/consecutive_ones_submatrix.rs b/src/models/algebraic/consecutive_ones_submatrix.rs index 8c8514e14..5e0896fa3 100644 --- a/src/models/algebraic/consecutive_ones_submatrix.rs +++ b/src/models/algebraic/consecutive_ones_submatrix.rs @@ -53,37 +53,50 @@ inventory::submit! { /// vec![true, false, true, true], /// vec![false, true, true, false], /// ]; -/// let problem = ConsecutiveOnesSubmatrix::new(matrix, 3); +/// let problem = ConsecutiveOnesSubmatrix::new(matrix, 3).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ConsecutiveOnesSubmatrixData")] pub struct ConsecutiveOnesSubmatrix { matrix: Vec>, bound: i64, } +#[derive(Deserialize)] +struct ConsecutiveOnesSubmatrixData { + matrix: Vec>, + bound: i64, +} + +impl TryFrom for ConsecutiveOnesSubmatrix { + type Error = crate::registry::ConstructionError; + + fn try_from(data: ConsecutiveOnesSubmatrixData) -> Result { + Self::new(data.matrix, data.bound) + } +} + impl ConsecutiveOnesSubmatrix { /// Create a new ConsecutiveOnesSubmatrix instance. /// - /// # Panics + /// # Errors /// - /// Panics if `bound > n`, or if rows have inconsistent lengths. - pub fn new(matrix: Vec>, bound: i64) -> Self { - let n = if matrix.is_empty() { - 0 - } else { - matrix[0].len() - }; - for row in &matrix { - assert_eq!(row.len(), n, "All rows must have the same length"); + /// Returns an error when matrix dimensions violate the instance definition. + pub fn new( + matrix: Vec>, + bound: i64, + ) -> Result { + let n = matrix.first().map_or(0, Vec::len); + if matrix.iter().any(|row| row.len() != n) { + return Err("all matrix rows must have the same length".into()); } - assert!( - bound < 0 || usize::try_from(bound).is_ok_and(|bound| bound <= n), - "bound ({bound}) must be <= number of columns ({n})" - ); - Self { matrix, bound } + if !(bound < 0 || usize::try_from(bound).is_ok_and(|bound| bound <= n)) { + return Err(format!("bound ({bound}) must be <= number of columns ({n})").into()); + } + Ok(Self { matrix, bound }) } /// Returns the binary matrix. @@ -215,8 +228,12 @@ impl Problem for ConsecutiveOnesSubmatrix { } impl crate::solvers::BruteForceProblem for ConsecutiveOnesSubmatrix { - fn dimensions(&self) -> Vec { - vec![2; self.num_cols()] + fn num_variables(&self) -> Result { + Ok(self.num_cols()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -234,14 +251,17 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec { - self.range_sets.iter().map(|m| m.len()).collect() + fn num_variables(&self) -> Result { + Ok(self.range_sets.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.range_sets[variable].len()) } } diff --git a/src/models/algebraic/feasible_basis_extension.rs b/src/models/algebraic/feasible_basis_extension.rs index 88396d820..15fbd8702 100644 --- a/src/models/algebraic/feasible_basis_extension.rs +++ b/src/models/algebraic/feasible_basis_extension.rs @@ -51,12 +51,13 @@ inventory::submit! { /// ]; /// let rhs = vec![7, 5, 3]; /// let required = vec![0, 1]; -/// let problem = FeasibleBasisExtension::new(matrix, rhs, required); +/// let problem = FeasibleBasisExtension::new(matrix, rhs, required).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "FeasibleBasisExtensionCreateSpec")] pub struct FeasibleBasisExtension { matrix: Vec>, rhs: Vec, @@ -79,26 +80,38 @@ struct FeasibleBasisExtensionCreateSpec { impl TryFrom for FeasibleBasisExtension { type Error = crate::registry::ConstructionError; fn try_from(spec: FeasibleBasisExtensionCreateSpec) -> Result { - let m = spec.matrix.len(); - let first = spec - .matrix - .first() - .ok_or("matrix must have at least one row")?; + Self::new(spec.matrix, spec.rhs, spec.required_columns) + } +} + +impl FeasibleBasisExtension { + /// Create a new FeasibleBasisExtension instance. + /// + /// # Errors + /// + /// Returns an error when dimensions or indices violate the instance definition. + pub fn new( + matrix: Vec>, + rhs: Vec, + required_columns: Vec, + ) -> Result { + let m = matrix.len(); + let first = matrix.first().ok_or("matrix must have at least one row")?; let n = first.len(); - if spec.matrix.iter().any(|row| row.len() != n) { + if matrix.iter().any(|row| row.len() != n) { return Err("all matrix rows must have the same length".into()); } if m >= n { return Err("number of rows must be less than number of columns".into()); } - if spec.rhs.len() != m { + if rhs.len() != m { return Err("rhs length must equal number of rows".into()); } - if spec.required_columns.len() >= m { + if required_columns.len() >= m { return Err("required_columns length must be less than number of rows".into()); } let mut seen = std::collections::HashSet::new(); - for &column in &spec.required_columns { + for &column in &required_columns { if column >= n { return Err(format!("required column {column} is out of bounds").into()); } @@ -107,66 +120,10 @@ impl TryFrom for FeasibleBasisExtension { } } Ok(Self { - matrix: spec.matrix, - rhs: spec.rhs, - required_columns: spec.required_columns, - }) - } -} - -impl FeasibleBasisExtension { - /// Create a new FeasibleBasisExtension instance. - /// - /// # Panics - /// - /// Panics if: - /// - The matrix is empty or has inconsistent row lengths - /// - m >= n (must have more columns than rows) - /// - rhs length does not equal m - /// - |S| >= m (must have room for at least one additional column) - /// - Any required column index is out of bounds - /// - Required columns contain duplicates - pub fn new(matrix: Vec>, rhs: Vec, required_columns: Vec) -> Self { - let m = matrix.len(); - assert!(m > 0, "Matrix must have at least one row"); - let n = matrix[0].len(); - for row in &matrix { - assert_eq!(row.len(), n, "All rows must have the same length"); - } - assert!( - m < n, - "Number of rows ({m}) must be less than number of columns ({n})" - ); - assert_eq!( - rhs.len(), - m, - "rhs length ({}) must equal number of rows ({m})", - rhs.len() - ); - assert!( - required_columns.len() < m, - "|S| ({}) must be less than m ({m})", - required_columns.len() - ); - for &col in &required_columns { - assert!(col < n, "Required column index {col} out of bounds (n={n})"); - } - // Check for duplicates - let mut sorted = required_columns.clone(); - sorted.sort_unstable(); - for i in 1..sorted.len() { - assert_ne!( - sorted[i - 1], - sorted[i], - "Duplicate required column index {}", - sorted[i] - ); - } - Self { matrix, rhs, required_columns, - } + }) } /// Returns the matrix A. @@ -463,8 +420,12 @@ impl Problem for FeasibleBasisExtension { } impl crate::solvers::BruteForceProblem for FeasibleBasisExtension { - fn dimensions(&self) -> Vec { - vec![2; self.num_columns() - self.num_required()] + fn num_variables(&self) -> Result { + Ok(self.num_columns() - self.num_required()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -481,15 +442,18 @@ pub(crate) fn canonical_model_example_specs() -> Vec B={0,1,2}, x=(4,5,3)>=0 - instance: Box::new(FeasibleBasisExtension::new( - vec![ - vec![1, 0, 1, 2, -1, 0], - vec![0, 1, 0, 1, 1, 2], - vec![0, 0, 1, 1, 0, 1], - ], - vec![7, 5, 3], - vec![0, 1], - )), + instance: Box::new( + FeasibleBasisExtension::new( + vec![ + vec![1, 0, 1, 2, -1, 0], + vec![0, 1, 0, 1, 1, 2], + vec![0, 0, 1, 1, 0, 1], + ], + vec![7, 5, 3], + vec![0, 1], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![true, false, false, false]), // select col 2 (first free column) optimal_value: serde_json::json!(true), }] diff --git a/src/models/algebraic/ilp.rs b/src/models/algebraic/ilp.rs index 7f4111e35..709e2299f 100644 --- a/src/models/algebraic/ilp.rs +++ b/src/models/algebraic/ilp.rs @@ -6,9 +6,7 @@ use crate::registry::{ConstructionError, FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::traits::{EvaluationError, Problem}; -use crate::types::{ - i64_to_exact_f64, Extremum, NumericArithmeticError, NumericSize, WeightElement, -}; +use crate::types::{Extremum, NumericArithmeticError, NumericSize, WeightElement}; use serde::{Deserialize, Deserializer, Serialize}; use std::fmt::Debug; use std::marker::PhantomData; @@ -80,19 +78,14 @@ impl ILPCoefficient for f64 { const NAME: &'static str = "f64"; fn from_integer(value: i64) -> Result { - i64_to_exact_f64(value).map_err(|_| { - EvaluationError::InexactFloatConversion( - "transporting an integer variable into an f64 ILP expression".into(), - ) - }) + Ok(value as f64) } fn satisfies(lhs: Self, comparison: Comparison, rhs: Self) -> bool { - let tolerance = 1e-9 * lhs.abs().max(rhs.abs()).max(1.0); match comparison { - Comparison::Le => lhs <= rhs + tolerance, - Comparison::Ge => lhs >= rhs - tolerance, - Comparison::Eq => (lhs - rhs).abs() <= tolerance, + Comparison::Le => lhs <= rhs, + Comparison::Ge => lhs >= rhs, + Comparison::Eq => lhs == rhs, } } } diff --git a/src/models/algebraic/minimum_matrix_cover.rs b/src/models/algebraic/minimum_matrix_cover.rs index 8df124c06..c969d25d5 100644 --- a/src/models/algebraic/minimum_matrix_cover.rs +++ b/src/models/algebraic/minimum_matrix_cover.rs @@ -3,7 +3,7 @@ //! Given an n×n nonnegative integer matrix A, find a sign assignment //! f: {1,...,n} → {-1,+1} minimizing Σ a_ij · f(i) · f(j). -use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::registry::{ConstructionError, FieldInfo, ProblemSchemaEntry}; use crate::traits::Problem; use crate::types::Min; use serde::{Deserialize, Serialize}; @@ -44,13 +44,13 @@ inventory::submit! { /// vec![3, 0, 0, 2], /// vec![1, 0, 0, 4], /// vec![0, 2, 4, 0], -/// ]); +/// ]).unwrap(); /// /// let solver = BruteForce::new(); /// let witness = solver.solve(&problem).unwrap(); /// assert!(witness.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumMatrixCover { /// The n×n nonnegative integer matrix. matrix: Vec>, @@ -59,20 +59,23 @@ pub struct MinimumMatrixCover { impl MinimumMatrixCover { /// Create a new MinimumMatrixCover instance. /// - /// # Panics - /// - /// Panics if the matrix is not square or has inconsistent row lengths. - pub fn new(matrix: Vec>) -> Self { + /// Returns an error for a nonsquare matrix or a negative entry. + pub fn new(matrix: Vec>) -> Result { let n = matrix.len(); for (i, row) in matrix.iter().enumerate() { - assert_eq!( - row.len(), - n, - "Matrix must be square: row {i} has {} columns, expected {n}", - row.len() - ); + if row.len() != n { + return Err(ConstructionError::InvalidInput(format!( + "matrix row {i} has {} columns, expected {n}", + row.len() + ))); + } + if row.iter().any(|&entry| entry < 0) { + return Err(ConstructionError::InvalidInput(format!( + "matrix row {i} contains a negative entry" + ))); + } } - Self { matrix } + Ok(Self { matrix }) } /// Returns the number of rows (= columns) of the matrix. @@ -86,6 +89,17 @@ impl MinimumMatrixCover { } } +impl<'de> Deserialize<'de> for MinimumMatrixCover { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + struct Data { + matrix: Vec>, + } + let data = Data::deserialize(deserializer)?; + Self::new(data.matrix).map_err(serde::de::Error::custom) + } +} + impl Problem for MinimumMatrixCover { const NAME: &'static str = "MinimumMatrixCover"; type Solution = Vec; @@ -140,8 +154,12 @@ impl Problem for MinimumMatrixCover { } impl crate::solvers::BruteForceProblem for MinimumMatrixCover { - fn dimensions(&self) -> Vec { - vec![2; self.num_rows()] + fn num_variables(&self) -> Result { + Ok(self.num_rows()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -159,12 +177,15 @@ pub(crate) fn canonical_model_example_specs() -> Vec>, @@ -61,28 +62,41 @@ pub struct MinimumMatrixDomination { ones: Vec<(usize, usize)>, } +#[derive(Deserialize)] +struct MinimumMatrixDominationData { + matrix: Vec>, +} + +impl TryFrom for MinimumMatrixDomination { + type Error = crate::registry::ConstructionError; + + fn try_from(data: MinimumMatrixDominationData) -> Result { + Self::new(data.matrix) + } +} + impl MinimumMatrixDomination { /// Create a new MinimumMatrixDomination instance. /// - /// # Panics + /// # Errors /// - /// Panics if the matrix rows have inconsistent lengths. - pub fn new(matrix: Vec>) -> Self { + /// Returns an error when matrix dimensions violate the instance definition. + pub fn new(matrix: Vec>) -> Result { let num_cols = matrix.first().map_or(0, Vec::len); - for row in &matrix { - assert_eq!(row.len(), num_cols, "All rows must have the same length"); + if matrix.iter().any(|row| row.len() != num_cols) { + return Err("all matrix rows must have the same length".into()); } - let ones: Vec<(usize, usize)> = matrix + let ones = matrix .iter() .enumerate() .flat_map(|(i, row)| { row.iter() .enumerate() - .filter(|(_, &v)| v) + .filter(|(_, &value)| value) .map(move |(j, _)| (i, j)) }) .collect(); - Self { matrix, ones } + Ok(Self { matrix, ones }) } /// Returns a reference to the binary matrix. @@ -174,8 +188,12 @@ impl Problem for MinimumMatrixDomination { } impl crate::solvers::BruteForceProblem for MinimumMatrixDomination { - fn dimensions(&self) -> Vec { - vec![2; self.num_ones()] + fn num_variables(&self) -> Result { + Ok(self.num_ones()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -202,7 +220,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec>, @@ -72,46 +73,31 @@ struct MinimumWeightDecodingCreateSpec { impl TryFrom for MinimumWeightDecoding { type Error = crate::registry::ConstructionError; fn try_from(spec: MinimumWeightDecodingCreateSpec) -> Result { - let first = spec - .matrix - .first() - .ok_or("matrix must have at least one row")?; - if first.is_empty() { - return Err("matrix must have at least one column".into()); - } - if spec.matrix.iter().any(|row| row.len() != first.len()) { - return Err("all matrix rows must have the same length".into()); - } - if spec.target.len() != spec.matrix.len() { - return Err("rhs length must equal number of rows".into()); - } - Ok(Self { - matrix: spec.matrix, - target: spec.target, - }) + Self::new(spec.matrix, spec.target) } } impl MinimumWeightDecoding { /// Create a new MinimumWeightDecoding instance. /// - /// # Panics + /// # Errors /// - /// Panics if the matrix is empty, rows have inconsistent lengths, - /// target length does not match the number of rows, or there are no columns. - pub fn new(matrix: Vec>, target: Vec) -> Self { - assert!(!matrix.is_empty(), "Matrix must have at least one row"); - let num_cols = matrix[0].len(); - assert!(num_cols > 0, "Matrix must have at least one column"); - for row in &matrix { - assert_eq!(row.len(), num_cols, "All rows must have the same length"); + /// Returns an error when dimensions or indices violate the instance definition. + pub fn new( + matrix: Vec>, + target: Vec, + ) -> Result { + let first = matrix.first().ok_or("matrix must have at least one row")?; + if first.is_empty() { + return Err("matrix must have at least one column".into()); + } + if matrix.iter().any(|row| row.len() != first.len()) { + return Err("all matrix rows must have the same length".into()); } - assert_eq!( - target.len(), - matrix.len(), - "Target length must equal number of rows" - ); - Self { matrix, target } + if target.len() != matrix.len() { + return Err("rhs length must equal number of rows".into()); + } + Ok(Self { matrix, target }) } /// Returns a reference to the parity-check matrix H. @@ -181,8 +167,12 @@ impl Problem for MinimumWeightDecoding { } impl crate::solvers::BruteForceProblem for MinimumWeightDecoding { - fn dimensions(&self) -> Vec { - vec![2; self.num_cols()] + fn num_variables(&self) -> Result { + Ok(self.num_cols()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -206,7 +196,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec>, @@ -71,46 +72,31 @@ struct MinimumWeightSolutionCreateSpec { impl TryFrom for MinimumWeightSolutionToLinearEquations { type Error = crate::registry::ConstructionError; fn try_from(spec: MinimumWeightSolutionCreateSpec) -> Result { - let first = spec - .matrix - .first() - .ok_or("matrix must have at least one row")?; - if first.is_empty() { - return Err("matrix must have at least one column".into()); - } - if spec.matrix.iter().any(|row| row.len() != first.len()) { - return Err("all matrix rows must have the same length".into()); - } - if spec.rhs.len() != spec.matrix.len() { - return Err("rhs length must equal number of rows".into()); - } - Ok(Self { - matrix: spec.matrix, - rhs: spec.rhs, - }) + Self::new(spec.matrix, spec.rhs) } } impl MinimumWeightSolutionToLinearEquations { /// Create a new MinimumWeightSolutionToLinearEquations instance. /// - /// # Panics + /// # Errors /// - /// Panics if the matrix is empty, rows have inconsistent lengths, - /// rhs length does not match the number of rows, or there are no columns. - pub fn new(matrix: Vec>, rhs: Vec) -> Self { - assert!(!matrix.is_empty(), "Matrix must have at least one row"); - let num_cols = matrix[0].len(); - assert!(num_cols > 0, "Matrix must have at least one column"); - for row in &matrix { - assert_eq!(row.len(), num_cols, "All rows must have the same length"); + /// Returns an error when dimensions or indices violate the instance definition. + pub fn new( + matrix: Vec>, + rhs: Vec, + ) -> Result { + let first = matrix.first().ok_or("matrix must have at least one row")?; + if first.is_empty() { + return Err("matrix must have at least one column".into()); + } + if matrix.iter().any(|row| row.len() != first.len()) { + return Err("all matrix rows must have the same length".into()); } - assert_eq!( - rhs.len(), - matrix.len(), - "RHS length must equal number of rows" - ); - Self { matrix, rhs } + if rhs.len() != matrix.len() { + return Err("rhs length must equal number of rows".into()); + } + Ok(Self { matrix, rhs }) } /// Returns a reference to the matrix A. @@ -259,8 +245,12 @@ impl Problem for MinimumWeightSolutionToLinearEquations { } impl crate::solvers::BruteForceProblem for MinimumWeightSolutionToLinearEquations { - fn dimensions(&self) -> Vec { - vec![2; self.num_variables()] + fn num_variables(&self) -> Result { + Ok(self.num_variables()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -281,7 +271,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec>, @@ -65,6 +66,20 @@ pub struct QuadraticAssignment { distance_matrix: Vec>, } +#[derive(Deserialize)] +struct QuadraticAssignmentData { + cost_matrix: Vec>, + distance_matrix: Vec>, +} + +impl TryFrom for QuadraticAssignment { + type Error = crate::registry::ConstructionError; + + fn try_from(data: QuadraticAssignmentData) -> Result { + Self::new(data.cost_matrix, data.distance_matrix) + } +} + impl QuadraticAssignment { /// Create a new Quadratic Assignment Problem. /// @@ -72,25 +87,28 @@ impl QuadraticAssignment { /// * `cost_matrix` - n x n matrix of flows/costs between facilities /// * `distance_matrix` - m x m matrix of distances between locations /// - /// # Panics - /// Panics if either matrix is not square, or if num_facilities > num_locations. - pub fn new(cost_matrix: Vec>, distance_matrix: Vec>) -> Self { + /// # Errors + /// + /// Returns an error when matrix dimensions violate the instance definition. + pub fn new( + cost_matrix: Vec>, + distance_matrix: Vec>, + ) -> Result { let n = cost_matrix.len(); - for row in &cost_matrix { - assert_eq!(row.len(), n, "cost_matrix must be square"); + if cost_matrix.iter().any(|row| row.len() != n) { + return Err("cost_matrix must be square".into()); } let m = distance_matrix.len(); - for row in &distance_matrix { - assert_eq!(row.len(), m, "distance_matrix must be square"); + if distance_matrix.iter().any(|row| row.len() != m) { + return Err("distance_matrix must be square".into()); } - assert!( - n <= m, - "num_facilities ({n}) must be <= num_locations ({m})" - ); - Self { + if n > m { + return Err(format!("num_facilities ({n}) must be <= num_locations ({m})").into()); + } + Ok(Self { cost_matrix, distance_matrix, - } + }) } /// Get the cost/flow matrix. @@ -186,8 +204,12 @@ impl Problem for QuadraticAssignment { } impl crate::solvers::BruteForceProblem for QuadraticAssignment { - fn dimensions(&self) -> Vec { - vec![self.num_locations(); self.num_facilities()] + fn num_variables(&self) -> Result { + Ok(self.num_facilities()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_locations()) } } @@ -203,20 +225,23 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "quadratic_assignment", - instance: Box::new(QuadraticAssignment::new( - vec![ - vec![0, 5, 2, 0], - vec![5, 0, 0, 3], - vec![2, 0, 0, 4], - vec![0, 3, 4, 0], - ], - vec![ - vec![0, 4, 1, 1], - vec![4, 0, 3, 4], - vec![1, 3, 0, 4], - vec![1, 4, 4, 0], - ], - )), + instance: Box::new( + QuadraticAssignment::new( + vec![ + vec![0, 5, 2, 0], + vec![5, 0, 0, 3], + vec![2, 0, 0, 4], + vec![0, 3, 4, 0], + ], + vec![ + vec![0, 4, 1, 1], + vec![4, 0, 3, 4], + vec![1, 3, 0, 4], + vec![1, 4, 4, 0], + ], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![3, 0, 1, 2]), optimal_value: serde_json::json!(56), }] diff --git a/src/models/algebraic/quadratic_congruences.rs b/src/models/algebraic/quadratic_congruences.rs index 98212273c..eb7a01d1d 100644 --- a/src/models/algebraic/quadratic_congruences.rs +++ b/src/models/algebraic/quadratic_congruences.rs @@ -247,13 +247,12 @@ impl Problem for QuadraticCongruences { } impl crate::solvers::BruteForceProblem for QuadraticCongruences { - fn dimensions(&self) -> Vec { - let num_bits = self.witness_bit_length(); - if num_bits == 0 { - Vec::new() - } else { - vec![2; num_bits] - } + fn num_variables(&self) -> Result { + Ok(self.witness_bit_length()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2) } } diff --git a/src/models/algebraic/quadratic_diophantine_equations.rs b/src/models/algebraic/quadratic_diophantine_equations.rs index 6cef6e290..2115b01b6 100644 --- a/src/models/algebraic/quadratic_diophantine_equations.rs +++ b/src/models/algebraic/quadratic_diophantine_equations.rs @@ -254,13 +254,12 @@ impl Problem for QuadraticDiophantineEquations { } impl crate::solvers::BruteForceProblem for QuadraticDiophantineEquations { - fn dimensions(&self) -> Vec { - let num_bits = self.witness_bit_length(); - if num_bits == 0 { - Vec::new() - } else { - vec![2; num_bits] - } + fn num_variables(&self) -> Result { + Ok(self.witness_bit_length()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2) } } diff --git a/src/models/algebraic/qubo.rs b/src/models/algebraic/qubo.rs index 1c145bd6e..86466e024 100644 --- a/src/models/algebraic/qubo.rs +++ b/src/models/algebraic/qubo.rs @@ -7,6 +7,8 @@ use crate::traits::Problem; use crate::types::{Min, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; +use sprs::CsMat; +use std::collections::BTreeMap; inventory::submit! { ProblemSchemaEntry { @@ -56,12 +58,24 @@ inventory::submit! { /// assert!(solutions.contains(&vec![false, true])); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde( + try_from = "QuboData", + bound(deserialize = "W: WeightElement + Deserialize<'de>") +)] pub struct QUBO { - /// Number of variables. - num_vars: usize, - /// Q matrix stored as upper triangular (row-major). - /// `Q[i][j]` for i <= j represents the coefficient of x_i * x_j - matrix: Vec>, + matrix: CsMat, +} + +#[derive(Deserialize)] +struct QuboData { + matrix: CsMat, +} + +impl TryFrom> for QUBO { + type Error = ConstructionError; + fn try_from(data: QuboData) -> Result { + Self::from_sparse(data.matrix) + } } #[derive(Debug, Deserialize, crate::CreateSpec)] @@ -95,12 +109,61 @@ impl QUBO { "QUBO matrix row {row} has length {actual}, expected {num_vars}" ))); } - for (row, values) in matrix.iter().enumerate() { - for (column, value) in values.iter().enumerate() { - value.validate_element(&format!("QUBO coefficient at ({row}, {column})"))?; + Self::from_rows( + matrix + .into_iter() + .map(|row| row.into_iter().enumerate()) + .collect(), + ) + } + + /// Create a QUBO from a square sparse matrix. Only its upper triangle is evaluated. + pub fn from_sparse(matrix: CsMat) -> Result { + if matrix.rows() != matrix.cols() { + return Err(ConstructionError::Conversion( + "QUBO matrix must be square".into(), + )); + } + let matrix = matrix.into_csr(); + for (row, values) in matrix.outer_iterator().enumerate() { + for (column, value) in values.iter() { + value + .validate_element("QUBO coefficient") + .map_err(|error| match error { + ConstructionError::NonFiniteFloat(message) => { + ConstructionError::NonFiniteFloat(format!( + "{message} at ({row}, {column})" + )) + } + error => error, + })?; } } - Ok(Self { num_vars, matrix }) + Ok(Self { matrix }) + } + + // Rows collect assignments and checked additions before compression. No library + // duplicate summation may replace the rule's numeric operations. + pub(crate) fn from_rows( + rows: Vec>, + ) -> Result { + let n = rows.len(); + let mut offsets = Vec::with_capacity(n + 1); + let mut indices = Vec::new(); + let mut values = Vec::new(); + offsets.push(0); + for row in rows { + for (column, value) in row { + if !value.to_sum().is_zero() { + indices.push(column); + values.push(value); + } + } + offsets.push(values.len()); + } + let matrix = CsMat::try_new((n, n), offsets, indices, values) + .map_err(|(_, _, _, error)| ConstructionError::Conversion(error.to_string()))?; + Self::from_sparse(matrix) } /// Create a QUBO from linear and quadratic terms. @@ -113,11 +176,11 @@ impl QUBO { quadratic: Vec<((usize, usize), W)>, ) -> Result { let num_vars = linear.len(); - let mut matrix = vec![vec![W::default(); num_vars]; num_vars]; + let mut matrix = vec![BTreeMap::new(); num_vars]; // Set diagonal (linear terms) for (i, val) in linear.into_iter().enumerate() { - matrix[i][i] = val; + matrix[i].insert(i, val); } // Set off-diagonal (quadratic terms) @@ -128,30 +191,34 @@ impl QUBO { ))); } if i < j { - matrix[i][j] = val; + matrix[i].insert(j, val); } else { - matrix[j][i] = val; + matrix[j].insert(i, val); } } - Self::from_matrix(matrix) + Self::from_rows(matrix) } } impl QUBO { /// Get the number of variables. pub fn num_vars(&self) -> usize { - self.num_vars + self.matrix.rows() } /// Get the Q matrix. - pub fn matrix(&self) -> &[Vec] { + pub fn matrix(&self) -> &CsMat { &self.matrix } - /// Get a specific matrix element `Q[i][j]`. - pub fn get(&self, i: usize, j: usize) -> Option<&W> { - self.matrix.get(i).and_then(|row| row.get(j)) + /// Get a coefficient, returning zero for an unstored entry and None outside the matrix. + pub fn get(&self, i: usize, j: usize) -> Option + where + W: Clone + Zero, + { + (i < self.num_vars() && j < self.num_vars()) + .then(|| self.matrix.get(i, j).cloned().unwrap_or_else(W::zero)) } } @@ -169,31 +236,26 @@ where &self, solution: &Self::Solution, ) -> Result, crate::traits::EvaluationError> { - if solution.len() != self.num_vars { + if solution.len() != self.num_vars() { return Err(crate::traits::EvaluationError::InvalidConfiguration( format!( "solution has {} variables, expected {}", solution.len(), - self.num_vars + self.matrix.rows() ), )); } let mut value = W::Sum::zero(); - for i in 0..self.num_vars { + for (i, row) in self.matrix.outer_iterator().enumerate() { if !solution[i] { continue; } - - for (j, &selected) in solution.iter().enumerate().skip(i) { - if !selected { - continue; - } - - if let Some(q_ij) = self.matrix.get(i).and_then(|row| row.get(j)) { + for (j, coefficient) in row.iter() { + if j >= i && solution[j] { value = W::checked_add_to_sum( value, - q_ij.to_sum(), + coefficient.to_sum(), "summing selected QUBO coefficients", )?; } @@ -212,8 +274,12 @@ impl crate::solvers::BruteForceProblem for QUBO where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/algebraic/simultaneous_incongruences.rs b/src/models/algebraic/simultaneous_incongruences.rs index 968d8deda..36565ace0 100644 --- a/src/models/algebraic/simultaneous_incongruences.rs +++ b/src/models/algebraic/simultaneous_incongruences.rs @@ -79,11 +79,6 @@ impl SimultaneousIncongruences { .into()); } } - pairs.iter().try_fold(1i64, |lcm, &(_, modulus)| { - (lcm / gcd(lcm, modulus)) - .checked_mul(modulus) - .ok_or_else(|| "Least common multiple of moduli exceeds i64 range".to_string()) - })?; Ok(()) } @@ -105,9 +100,15 @@ impl SimultaneousIncongruences { } /// Compute the LCM of all moduli. - pub fn lcm_moduli(&self) -> i64 { - self.pairs.iter().fold(1i64, |lcm, &(_, modulus)| { - (lcm / gcd(lcm, modulus)) * modulus + pub fn lcm_moduli(&self) -> Result { + self.pairs.iter().try_fold(1i64, |lcm, &(_, modulus)| { + (lcm / gcd(lcm, modulus)) + .checked_mul(modulus) + .ok_or_else(|| { + crate::traits::EvaluationError::IntegerOverflow( + "computing the incongruence period".into(), + ) + }) }) } } @@ -141,15 +142,18 @@ impl Problem for SimultaneousIncongruences { fn evaluate(&self, solution: &Self::Solution) -> Result { Ok({ // x is a solution iff x % bᵢ ≠ aᵢ % bᵢ for every pair. - Or(self.pairs.iter().all(|&(a, b)| solution % b != a % b)) + Or(*solution >= 0 && self.pairs.iter().all(|&(a, b)| solution % b != a % b)) }) } } impl crate::solvers::BruteForceProblem for SimultaneousIncongruences { - fn dimensions(&self) -> Vec { - let lcm = usize::try_from(self.lcm_moduli()).expect("validated positive LCM fits usize"); - vec![lcm] + fn num_variables(&self) -> Result { + Ok(1) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(usize::try_from(self.lcm_moduli()?)?) } } diff --git a/src/models/algebraic/sparse_matrix_compression.rs b/src/models/algebraic/sparse_matrix_compression.rs index 724a0aff7..dc77dfa7d 100644 --- a/src/models/algebraic/sparse_matrix_compression.rs +++ b/src/models/algebraic/sparse_matrix_compression.rs @@ -28,6 +28,7 @@ inventory::submit! { /// enumerating storage-vector entries directly, so brute-force search runs over /// `bound_k ^ num_rows` shift assignments. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "SparseMatrixCompressionCreateSpec")] pub struct SparseMatrixCompression { matrix: Vec>, bound_k: usize, @@ -44,16 +45,7 @@ struct SparseMatrixCompressionCreateSpec { impl TryFrom for SparseMatrixCompression { type Error = crate::registry::ConstructionError; fn try_from(spec: SparseMatrixCompressionCreateSpec) -> Result { - if spec.bound_k == 0 { - return Err("bound_k must be positive".to_string().into()); - } - let columns = spec.matrix.first().map_or(0, Vec::len); - if spec.matrix.iter().any(|row| row.len() != columns) { - return Err("all matrix rows must have the same length" - .to_string() - .into()); - } - Ok(Self::new(spec.matrix, spec.bound_k)) + Self::new(spec.matrix, spec.bound_k) } } @@ -63,15 +55,20 @@ impl SparseMatrixCompression { /// # Panics /// /// Panics if `bound_k == 0` or if the matrix rows are ragged. - pub fn new(matrix: Vec>, bound_k: usize) -> Self { - assert!(bound_k > 0, "bound_k must be positive"); - - let num_cols = matrix.first().map_or(0, Vec::len); - for row in &matrix { - assert_eq!(row.len(), num_cols, "All rows must have the same length"); + pub fn new( + matrix: Vec>, + bound_k: usize, + ) -> Result { + if bound_k == 0 { + return Err("bound_k must be positive".to_string().into()); } - - Self { matrix, bound_k } + let columns = matrix.first().map_or(0, Vec::len); + if matrix.iter().any(|row| row.len() != columns) { + return Err("all matrix rows must have the same length" + .to_string() + .into()); + } + Ok(Self { matrix, bound_k }) } /// Return the binary matrix. @@ -173,8 +170,12 @@ impl Problem for SparseMatrixCompression { } impl crate::solvers::BruteForceProblem for SparseMatrixCompression { - fn dimensions(&self) -> Vec { - vec![self.bound_k; self.num_rows()] + fn num_variables(&self) -> Result { + Ok(self.num_rows()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.bound_k) } } @@ -190,15 +191,18 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "sparse_matrix_compression", - instance: Box::new(SparseMatrixCompression::new( - vec![ - vec![true, false, false, true], - vec![false, true, false, false], - vec![false, false, true, false], - vec![true, false, false, false], - ], - 2, - )), + instance: Box::new( + SparseMatrixCompression::new( + vec![ + vec![true, false, false, true], + vec![false, true, false, false], + vec![false, false, true, false], + vec![true, false, false, false], + ], + 2, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![1, 1, 1, 0]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/decision.rs b/src/models/decision.rs index 1acb637ca..be8918fed 100644 --- a/src/models/decision.rs +++ b/src/models/decision.rs @@ -96,7 +96,21 @@ macro_rules! register_decision_variant { >)?; let result = <$crate::models::decision::Decision<$inner> as $crate::rules::ReduceTo<$inner>>::reduce_to(source)?; - Ok(Box::new(result)) + let result = std::rc::Rc::new(result); + Ok($crate::rules::registry::ExecutedStep { + aggregate: Some(result.clone()), + interpret_optimum: Some({ + let result = result.clone(); + std::rc::Rc::new(move |solution: &dyn std::any::Any| { + let solution = solution.downcast_ref::<<$inner as $crate::traits::Problem>::Solution>() + .ok_or_else(|| $crate::rules::ExtractionError::invalid("target solution type mismatch"))?; + let target = $crate::rules::ReductionResult::target_problem(result.as_ref()); + let value = $crate::traits::Problem::evaluate(target, solution)?; + Ok($crate::rules::AggregateReductionResult::extract_value(result.as_ref(), value).is_valid()) + }) + }), + witness: result, + }) }), reduce_aggregate_fn: Some(|any| { let source = any @@ -317,12 +331,21 @@ where P: DecisionProblemMeta + crate::solvers::BruteForceProblem, P::Value: OptimizationValue, { - fn dimensions(&self) -> Vec { - self.inner.dimensions() + fn num_variables(&self) -> Result { + self.inner.num_variables() + } + + fn dimension(&self, variable: usize) -> Result { + self.inner.dimension(variable) } } -/// Aggregate reduction result for `Decision

-> P`. +/// Executed reduction from `Decision

` to its optimization problem. +/// +/// The target and decision bound belong to the same execution. An optimum +/// meeting the bound supplies a decision witness; an optimum missing the bound +/// establishes NO through `extract_value`. Witness extraction copies a target +/// witness that meets the bound and does not repeat the comparison. #[derive(Debug, Clone)] pub struct DecisionToOptimizationResult

where @@ -368,25 +391,11 @@ where } } -/// Witness reduction result for `Decision

-> P`. -/// -/// The configuration spaces are identical — a config that is optimal for -/// `P` and meets the bound is a valid `Decision

` witness. The -/// `extract_solution` is the identity function. -#[derive(Debug, Clone)] -pub struct DecisionToOptimizationWitnessResult

-where - P: Problem, - P::Value: OptimizationValue, -{ - target: P, -} - -impl

ReductionResult for DecisionToOptimizationWitnessResult

+impl

ReductionResult for DecisionToOptimizationResult

where P: DecisionProblemMeta + 'static, P::Solution: Clone, - P::Value: OptimizationValue + Serialize + DeserializeOwned, + P::Value: OptimizationValue, { type Source = Decision

; type Target = P; @@ -399,8 +408,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.clone()) } } @@ -409,13 +416,14 @@ impl

ReduceTo

for Decision

where P: DecisionProblemMeta + Clone + 'static, P::Solution: Clone, - P::Value: OptimizationValue + Serialize + DeserializeOwned, + P::Value: OptimizationValue, { - type Result = DecisionToOptimizationWitnessResult

; + type Result = DecisionToOptimizationResult

; fn reduce_to(&self) -> Result { - Ok(DecisionToOptimizationWitnessResult { + Ok(DecisionToOptimizationResult { target: self.inner.clone(), + bound: self.bound.clone(), }) } } diff --git a/src/models/formula/circuit.rs b/src/models/formula/circuit.rs index 2e078c074..1d20f16e2 100644 --- a/src/models/formula/circuit.rs +++ b/src/models/formula/circuit.rs @@ -358,8 +358,12 @@ impl Problem for CircuitSAT { } impl crate::solvers::BruteForceProblem for CircuitSAT { - fn dimensions(&self) -> Vec { - vec![2; self.variables.len()] + fn num_variables(&self) -> Result { + Ok(self.variables.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/formula/ksat.rs b/src/models/formula/ksat.rs index dede5be57..2c7302f44 100644 --- a/src/models/formula/ksat.rs +++ b/src/models/formula/ksat.rs @@ -267,8 +267,12 @@ impl Problem for KSatisfiability { } impl crate::solvers::BruteForceProblem for KSatisfiability { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/formula/maximum_2_satisfiability.rs b/src/models/formula/maximum_2_satisfiability.rs index 5085b21ff..98d6bc3ba 100644 --- a/src/models/formula/maximum_2_satisfiability.rs +++ b/src/models/formula/maximum_2_satisfiability.rs @@ -141,8 +141,12 @@ impl Problem for Maximum2Satisfiability { } impl crate::solvers::BruteForceProblem for Maximum2Satisfiability { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/formula/nae_satisfiability.rs b/src/models/formula/nae_satisfiability.rs index f65fdb717..8e38248d6 100644 --- a/src/models/formula/nae_satisfiability.rs +++ b/src/models/formula/nae_satisfiability.rs @@ -186,8 +186,12 @@ impl Problem for NAESatisfiability { } impl crate::solvers::BruteForceProblem for NAESatisfiability { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/formula/non_tautology.rs b/src/models/formula/non_tautology.rs index a45f40aab..66d8af9ca 100644 --- a/src/models/formula/non_tautology.rs +++ b/src/models/formula/non_tautology.rs @@ -177,8 +177,12 @@ impl Problem for NonTautology { } impl crate::solvers::BruteForceProblem for NonTautology { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/formula/one_in_three_satisfiability.rs b/src/models/formula/one_in_three_satisfiability.rs index f78004e32..653ca9043 100644 --- a/src/models/formula/one_in_three_satisfiability.rs +++ b/src/models/formula/one_in_three_satisfiability.rs @@ -154,8 +154,12 @@ impl Problem for OneInThreeSatisfiability { } impl crate::solvers::BruteForceProblem for OneInThreeSatisfiability { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/formula/planar_3_satisfiability.rs b/src/models/formula/planar_3_satisfiability.rs index f32349663..f1c2cf77d 100644 --- a/src/models/formula/planar_3_satisfiability.rs +++ b/src/models/formula/planar_3_satisfiability.rs @@ -150,8 +150,12 @@ impl Problem for Planar3Satisfiability { } impl crate::solvers::BruteForceProblem for Planar3Satisfiability { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/formula/qbf.rs b/src/models/formula/qbf.rs index 4aec9a7d7..fa641eb83 100644 --- a/src/models/formula/qbf.rs +++ b/src/models/formula/qbf.rs @@ -187,8 +187,12 @@ impl Problem for QuantifiedBooleanFormulas { } impl crate::solvers::BruteForceProblem for QuantifiedBooleanFormulas { - fn dimensions(&self) -> Vec { - vec![] + fn num_variables(&self) -> Result { + Ok(0) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(0) } } diff --git a/src/models/formula/sat.rs b/src/models/formula/sat.rs index fb0b6ff5f..4fdf99701 100644 --- a/src/models/formula/sat.rs +++ b/src/models/formula/sat.rs @@ -233,8 +233,12 @@ impl Problem for Satisfiability { } impl crate::solvers::BruteForceProblem for Satisfiability { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/acyclic_partition.rs b/src/models/graph/acyclic_partition.rs index 2ed924353..205376492 100644 --- a/src/models/graph/acyclic_partition.rs +++ b/src/models/graph/acyclic_partition.rs @@ -29,7 +29,7 @@ inventory::submit! { } /// Acyclic Partition (Garey & Johnson ND15). -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct AcyclicPartition { graph: DirectedGraph, vertex_weights: Vec, @@ -38,6 +38,34 @@ pub struct AcyclicPartition { cost_bound: W::Sum, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>"))] +struct AcyclicPartitionData { + graph: DirectedGraph, + vertex_weights: Vec, + arc_costs: Vec, + weight_bound: W::Sum, + cost_bound: W::Sum, +} + +impl<'de, W> Deserialize<'de> for AcyclicPartition +where + W: WeightElement + Deserialize<'de>, + W::Sum: Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = AcyclicPartitionData::::deserialize(deserializer)?; + Self::new( + data.graph, + data.vertex_weights, + data.arc_costs, + data.weight_bound, + data.cost_bound, + ) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct AcyclicPartitionCreateSpec { #[create(codec = "arc-list")] @@ -69,38 +97,18 @@ impl TryFrom for AcyclicPartition { .transpose()? .unwrap_or(0); let num_vertices = spec.num_vertices.unwrap_or(inferred); - if num_vertices < inferred { - return Err(format!( - "num_vertices {num_vertices} is too small for arc endpoints; need at least {inferred}" - ).into()); - } - let graph = DirectedGraph::new(num_vertices, spec.arcs); + let graph = DirectedGraph::new(num_vertices, spec.arcs)?; let vertex_weights = spec.weights.unwrap_or_else(|| vec![1; num_vertices]); - if vertex_weights.len() != num_vertices { - return Err(format!( - "weights has length {}, expected {num_vertices}", - vertex_weights.len() - ) - .into()); - } let arc_costs = spec .arc_weights .unwrap_or_else(|| vec![1; graph.num_arcs()]); - if arc_costs.len() != graph.num_arcs() { - return Err(format!( - "arc_weights has length {}, expected {}", - arc_costs.len(), - graph.num_arcs() - ) - .into()); - } - Ok(Self::new( + Self::new( graph, vertex_weights, arc_costs, spec.weight_bound, spec.cost_bound, - )) + ) } } @@ -112,24 +120,16 @@ impl AcyclicPartition { arc_costs: Vec, weight_bound: W::Sum, cost_bound: W::Sum, - ) -> Self { - assert_eq!( - vertex_weights.len(), - graph.num_vertices(), - "vertex_weights length must match graph num_vertices" - ); - assert_eq!( - arc_costs.len(), - graph.num_arcs(), - "arc_costs length must match graph num_arcs" - ); - Self { + ) -> Result { + Self::check_vertex_weights(&graph, &vertex_weights)?; + Self::check_arc_costs(&graph, &arc_costs)?; + Ok(Self { graph, vertex_weights, arc_costs, weight_bound, cost_bound, - } + }) } /// Get the underlying graph. @@ -148,23 +148,43 @@ impl AcyclicPartition { } /// Replace the vertex weights. - pub fn set_vertex_weights(&mut self, vertex_weights: Vec) { - assert_eq!( - vertex_weights.len(), - self.graph.num_vertices(), - "vertex_weights length must match graph num_vertices" - ); + pub fn set_vertex_weights( + &mut self, + vertex_weights: Vec, + ) -> Result<(), crate::registry::ConstructionError> { + Self::check_vertex_weights(&self.graph, &vertex_weights)?; self.vertex_weights = vertex_weights; + Ok(()) + } + + fn check_vertex_weights( + graph: &DirectedGraph, + vertex_weights: &[W], + ) -> Result<(), crate::registry::ConstructionError> { + if vertex_weights.len() != graph.num_vertices() { + return Err("vertex_weights length must match graph num_vertices".into()); + } + Ok(()) } /// Replace the arc costs. - pub fn set_arc_costs(&mut self, arc_costs: Vec) { - assert_eq!( - arc_costs.len(), - self.graph.num_arcs(), - "arc_costs length must match graph num_arcs" - ); + pub fn set_arc_costs( + &mut self, + arc_costs: Vec, + ) -> Result<(), crate::registry::ConstructionError> { + Self::check_arc_costs(&self.graph, &arc_costs)?; self.arc_costs = arc_costs; + Ok(()) + } + + fn check_arc_costs( + graph: &DirectedGraph, + arc_costs: &[W], + ) -> Result<(), crate::registry::ConstructionError> { + if arc_costs.len() != graph.num_arcs() { + return Err("arc_costs length must match graph num_arcs".into()); + } + Ok(()) } /// Get the per-part weight bound. @@ -256,8 +276,12 @@ impl crate::solvers::BruteForceProblem for AcyclicPartition where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.graph.num_vertices(); self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } @@ -322,7 +346,11 @@ fn is_valid_acyclic_partition( quotient_arcs.insert((dense_label[source_label], dense_label[target_label])); } - Ok(DirectedGraph::new(next_dense, quotient_arcs.into_iter().collect()).is_dag()) + Ok( + DirectedGraph::new(next_dense, quotient_arcs.into_iter().collect()) + .expect("quotient arc endpoints are densely numbered") + .is_dag(), + ) } crate::declare_variants! { @@ -337,25 +365,29 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "acyclic_partition", - instance: Box::new(AcyclicPartition::new( - DirectedGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (1, 3), - (1, 4), - (2, 4), - (2, 5), - (3, 5), - (4, 5), - ], - ), - vec![2, 3, 2, 1, 3, 1], - vec![1; 8], - 5, - 5, - )), + instance: Box::new( + AcyclicPartition::new( + DirectedGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 3), + (1, 4), + (2, 4), + (2, 5), + (3, 5), + (4, 5), + ], + ) + .unwrap(), + vec![2, 3, 2, 1, 3, 1], + vec![1; 8], + 5, + 5, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![0, 1, 0, 2, 2, 2]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/balanced_complete_bipartite_subgraph.rs b/src/models/graph/balanced_complete_bipartite_subgraph.rs index 538bdbb82..f53722a90 100644 --- a/src/models/graph/balanced_complete_bipartite_subgraph.rs +++ b/src/models/graph/balanced_complete_bipartite_subgraph.rs @@ -43,22 +43,8 @@ impl TryFrom for BalancedCompleteBi type Error = crate::registry::ConstructionError; fn try_from(spec: BalancedCompleteBipartiteSubgraphCreateSpec) -> Result { - for (index, &(left, right)) in spec.biedges.iter().enumerate() { - if left >= spec.left { - return Err(format!( - "biedges[{index}] left vertex {left} is out of bounds for left partition size {}", - spec.left - ).into()); - } - if right >= spec.right { - return Err(format!( - "biedges[{index}] right vertex {right} is out of bounds for right partition size {}", - spec.right - ).into()); - } - } Ok(Self::new( - BipartiteGraph::new(spec.left, spec.right, spec.biedges), + BipartiteGraph::new(spec.left, spec.right, spec.biedges)?, spec.k, )) } @@ -177,8 +163,12 @@ impl Problem for BalancedCompleteBipartiteSubgraph { } impl crate::solvers::BruteForceProblem for BalancedCompleteBipartiteSubgraph { - fn dimensions(&self) -> Vec { - vec![2; self.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -224,7 +214,8 @@ pub(crate) fn canonical_model_example_specs() -> Vec for BicliqueCover { type Error = crate::registry::ConstructionError; fn try_from(spec: BicliqueCoverCreateSpec) -> Result { - for (edge_index, &(left_vertex, right_vertex)) in spec.biedges.iter().enumerate() { - if left_vertex >= spec.left { - return Err(format!( - "biedges[{edge_index}] left vertex {left_vertex} is out of bounds for left partition size {}", - spec.left - ).into()); - } - if right_vertex >= spec.right { - return Err(format!( - "biedges[{edge_index}] right vertex {right_vertex} is out of bounds for right partition size {}", - spec.right - ).into()); - } - } - - let graph = BipartiteGraph::new(spec.left, spec.right, spec.biedges); + let graph = BipartiteGraph::new(spec.left, spec.right, spec.biedges)?; Ok(Self::new(graph, spec.k)) } } @@ -116,7 +101,10 @@ impl BicliqueCover { /// Create from a bipartite adjacency matrix. /// /// `Matrix[i][j] = 1` means edge between left vertex i and right vertex j. - pub fn from_matrix(matrix: &[Vec], k: usize) -> Self { + pub fn from_matrix( + matrix: &[Vec], + k: usize, + ) -> Result { let left_size = matrix.len(); let right_size = if left_size > 0 { matrix[0].len() } else { 0 }; @@ -129,10 +117,10 @@ impl BicliqueCover { } } - Self { - graph: BipartiteGraph::new(left_size, right_size, edges), + Ok(Self { + graph: BipartiteGraph::new(left_size, right_size, edges)?, k, - } + }) } /// Get the bipartite graph. @@ -365,9 +353,14 @@ impl Problem for BicliqueCover { } impl crate::solvers::BruteForceProblem for BicliqueCover { - fn dimensions(&self) -> Vec { - // Each vertex has k binary variables (one per biclique) - vec![2; self.num_vertices() * self.k] + fn num_variables(&self) -> Result { + (self.num_vertices()).checked_mul(self.k).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -389,7 +382,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec { + graph: G, + potential_weights: Vec<(usize, usize, W)>, + budget: W::Sum, +} + +impl<'de, G, W> Deserialize<'de> for BiconnectivityAugmentation +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, + W::Sum: Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = BiconnectivityAugmentationData::::deserialize(deserializer)?; + Self::new(data.graph, data.potential_weights, data.budget).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct BiconnectivityAugmentationCreateSpec { #[create(codec = "edge-list")] @@ -82,73 +106,56 @@ impl TryFrom .transpose()? .unwrap_or(0); let count = spec.num_vertices.unwrap_or(inferred); - if count < inferred { - return Err("num_vertices is too small for graph endpoints".into()); - } - let graph = SimpleGraph::new(count, spec.graph); - let mut seen = BTreeSet::new(); - for &(u, v, _) in &spec.potential_weights { - if u >= count || v >= count { - return Err("potential edge endpoint is out of bounds".into()); - } - if u == v { - return Err("potential edge is a self-loop".into()); - } - let edge = normalize_edge(u, v); - if graph.has_edge(edge.0, edge.1) { - return Err("potential edge already exists in graph".into()); - } - if !seen.insert(edge) { - return Err("duplicate potential edge".into()); - } - } - Ok(Self { - graph, - potential_weights: spec.potential_weights, - budget: spec.budget, - }) + let graph = SimpleGraph::new(count, spec.graph)?; + Self::new(graph, spec.potential_weights, spec.budget) } } impl BiconnectivityAugmentation { /// Create a new biconnectivity augmentation instance. /// - /// # Panics - /// Panics if any potential edge references a vertex index outside the graph, + /// # Errors + /// Returns an error if any potential edge references a vertex index outside the graph, /// is a self-loop, duplicates another candidate edge, or already exists in /// the input graph. - pub fn new(graph: G, potential_weights: Vec<(usize, usize, W)>, budget: W::Sum) -> Self { + pub fn new( + graph: G, + potential_weights: Vec<(usize, usize, W)>, + budget: W::Sum, + ) -> Result { let num_vertices = graph.num_vertices(); let mut seen_potential_edges = BTreeSet::new(); for &(u, v, _) in &potential_weights { - assert!( - u < num_vertices && v < num_vertices, - "potential edge ({}, {}) references vertex >= num_vertices ({})", - u, - v, - num_vertices - ); - assert!(u != v, "potential edge ({}, {}) is a self-loop", u, v); + if !(u < num_vertices && v < num_vertices) { + return Err(format!( + "potential edge ({}, {}) references vertex >= num_vertices ({})", + u, v, num_vertices + ) + .into()); + } + if u == v { + return Err(format!("potential edge ({}, {}) is a self-loop", u, v).into()); + } let edge = normalize_edge(u, v); - assert!( - !graph.has_edge(edge.0, edge.1), - "potential edge ({}, {}) already exists in the graph", - edge.0, - edge.1 - ); - assert!( - seen_potential_edges.insert(edge), - "potential edge ({}, {}) is duplicated", - edge.0, - edge.1 - ); + if !(!graph.has_edge(edge.0, edge.1)) { + return Err(format!( + "potential edge ({}, {}) already exists in the graph", + edge.0, edge.1 + ) + .into()); + } + if !(seen_potential_edges.insert(edge)) { + return Err( + format!("potential edge ({}, {}) is duplicated", edge.0, edge.1).into(), + ); + } } - Self { + Ok(Self { graph, potential_weights, budget, - } + }) } /// Get a reference to the underlying graph. @@ -189,7 +196,7 @@ impl BiconnectivityAugmentation { fn augmented_graph( &self, config: &[bool], - ) -> Result, crate::traits::EvaluationError> { + ) -> Result>, crate::traits::EvaluationError> { if config.len() != self.num_potential_edges() { return Ok(None); } @@ -215,10 +222,14 @@ impl BiconnectivityAugmentation { return Ok(None); } - Ok(Some(SimpleGraph::new( - self.num_vertices(), - edges.into_iter().collect(), - ))) + let mut graph = UnGraph::new_undirected(); + for _ in 0..self.num_vertices() { + graph.add_node(()); + } + for (u, v) in edges { + graph.add_edge(NodeIndex::new(u), NodeIndex::new(v), ()); + } + Ok(Some(graph)) } } @@ -264,8 +275,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.num_potential_edges()] + fn num_variables(&self) -> Result { + Ok(self.num_potential_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -277,67 +292,9 @@ fn normalize_edge(u: usize, v: usize) -> (usize, usize) { } } -struct DfsState { - visited: Vec, - discovery_time: Vec, - low: Vec, - parent: Vec>, - time: usize, - has_articulation_point: bool, -} - -fn dfs_articulation_points(graph: &G, vertex: usize, state: &mut DfsState) { - if state.has_articulation_point { - return; - } - - state.visited[vertex] = true; - state.time += 1; - state.discovery_time[vertex] = state.time; - state.low[vertex] = state.time; - - let mut child_count = 0; - for neighbor in graph.neighbors(vertex) { - if !state.visited[neighbor] { - child_count += 1; - state.parent[neighbor] = Some(vertex); - dfs_articulation_points(graph, neighbor, state); - state.low[vertex] = state.low[vertex].min(state.low[neighbor]); - - if state.parent[vertex].is_none() && child_count > 1 { - state.has_articulation_point = true; - return; - } - - if state.parent[vertex].is_some() && state.low[neighbor] >= state.discovery_time[vertex] - { - state.has_articulation_point = true; - return; - } - } else if state.parent[vertex] != Some(neighbor) { - state.low[vertex] = state.low[vertex].min(state.discovery_time[neighbor]); - } - } -} - -fn is_biconnected(graph: &G) -> bool { - let num_vertices = graph.num_vertices(); - if num_vertices <= 1 { - return true; - } - - let mut state = DfsState { - visited: vec![false; num_vertices], - discovery_time: vec![0; num_vertices], - low: vec![0; num_vertices], - parent: vec![None; num_vertices], - time: 0, - has_articulation_point: false, - }; - - dfs_articulation_points(graph, 0, &mut state); - - !state.has_articulation_point && state.visited.into_iter().all(|seen| seen) +fn is_biconnected(graph: &UnGraph<(), ()>) -> bool { + graph.node_count() <= 1 + || (connected_components(graph) == 1 && articulation_points(graph).is_empty()) } crate::declare_variants! { @@ -352,21 +309,24 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "biconnectivity_augmentation", - instance: Box::new(BiconnectivityAugmentation::new( - SimpleGraph::path(6), - vec![ - (0, 2, 1), - (0, 3, 2), - (0, 4, 3), - (1, 3, 1), - (1, 4, 2), - (1, 5, 3), - (2, 4, 1), - (2, 5, 2), - (3, 5, 1), - ], - 4, - )), + instance: Box::new( + BiconnectivityAugmentation::new( + SimpleGraph::path(6), + vec![ + (0, 2, 1), + (0, 3, 2), + (0, 4, 3), + (1, 3, 1), + (1, 4, 2), + (1, 5, 3), + (2, 4, 1), + (2, 5, 2), + (3, 5, 1), + ], + 4, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![ true, false, false, true, false, false, true, false, true ]), @@ -391,6 +351,7 @@ pub(crate) fn example_instance() -> BiconnectivityAugmentation ], 4, ) + .unwrap() } #[cfg(test)] diff --git a/src/models/graph/bottleneck_traveling_salesman.rs b/src/models/graph/bottleneck_traveling_salesman.rs index fd5aaac2c..0c82526b6 100644 --- a/src/models/graph/bottleneck_traveling_salesman.rs +++ b/src/models/graph/bottleneck_traveling_salesman.rs @@ -24,11 +24,25 @@ inventory::submit! { /// The Bottleneck Traveling Salesman problem on a simple weighted graph. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "BottleneckTravelingSalesmanData")] pub struct BottleneckTravelingSalesman { graph: SimpleGraph, edge_weights: Vec, } +#[derive(Deserialize)] +struct BottleneckTravelingSalesmanData { + graph: SimpleGraph, + edge_weights: Vec, +} + +impl TryFrom for BottleneckTravelingSalesman { + type Error = crate::registry::ConstructionError; + fn try_from(data: BottleneckTravelingSalesmanData) -> Result { + Self::new(data.graph, data.edge_weights) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct BottleneckTravelingSalesmanCreateSpec { #[create(codec = "edge-list")] @@ -46,15 +60,7 @@ impl TryFrom for BottleneckTravelingSales let edge_weights = spec .edge_weights .unwrap_or_else(|| vec![1; graph.num_edges()]); - if edge_weights.len() != graph.num_edges() { - return Err(format!( - "edge_weights has length {}, expected {}", - edge_weights.len(), - graph.num_edges() - ) - .into()); - } - Ok(Self::new(graph, edge_weights)) + Self::new(graph, edge_weights) } } @@ -86,21 +92,20 @@ fn simple_graph_from_create( ) .into()); } - Ok(SimpleGraph::new(num_vertices, edges)) + SimpleGraph::new(num_vertices, edges) } impl BottleneckTravelingSalesman { /// Create a BottleneckTravelingSalesman problem from a graph with edge weights. - pub fn new(graph: SimpleGraph, edge_weights: Vec) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - Self { + pub fn new( + graph: SimpleGraph, + edge_weights: Vec, + ) -> Result { + Self::check_weights(&graph, &edge_weights)?; + Ok(Self { graph, edge_weights, - } + }) } /// Get a reference to the underlying graph. @@ -114,9 +119,22 @@ impl BottleneckTravelingSalesman { } /// Set new weights for the problem. - pub fn set_weights(&mut self, weights: Vec) { - assert_eq!(weights.len(), self.graph.num_edges()); + pub fn set_weights( + &mut self, + weights: Vec, + ) -> Result<(), crate::registry::ConstructionError> { + Self::check_weights(&self.graph, &weights)?; self.edge_weights = weights; + Ok(()) + } + fn check_weights( + graph: &SimpleGraph, + weights: &[i64], + ) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_edges() { + return Err("edge_weights length must match num_edges".into()); + } + Ok(()) } /// Get all edges with their weights. @@ -192,8 +210,12 @@ impl Problem for BottleneckTravelingSalesman { } impl crate::solvers::BruteForceProblem for BottleneckTravelingSalesman { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -201,24 +223,28 @@ impl crate::solvers::BruteForceProblem for BottleneckTravelingSalesman { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "bottleneck_traveling_salesman", - instance: Box::new(BottleneckTravelingSalesman::new( - SimpleGraph::new( - 5, - vec![ - (0, 1), - (0, 2), - (0, 3), - (0, 4), - (1, 2), - (1, 3), - (1, 4), - (2, 3), - (2, 4), - (3, 4), - ], - ), - vec![5, 4, 4, 5, 4, 1, 2, 1, 5, 4], - )), + instance: Box::new( + BottleneckTravelingSalesman::new( + SimpleGraph::new( + 5, + vec![ + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (1, 2), + (1, 3), + (1, 4), + (2, 3), + (2, 4), + (3, 4), + ], + ) + .unwrap(), + vec![5, 4, 4, 5, 4, 1, 2, 1, 5, 4], + ) + .unwrap(), + ), optimal_config: serde_json::json!([ false, true, true, false, true, false, true, false, false, true ]), @@ -232,7 +258,7 @@ crate::impl_random_generate!( |spec| { let graph = spec.graph()?; let weights = vec![1; graph.num_edges()]; - Ok(BottleneckTravelingSalesman::new(graph, weights)) + Ok(BottleneckTravelingSalesman::new(graph, weights).unwrap()) } ); diff --git a/src/models/graph/bounded_component_spanning_forest.rs b/src/models/graph/bounded_component_spanning_forest.rs index 155532cbc..c2363c259 100644 --- a/src/models/graph/bounded_component_spanning_forest.rs +++ b/src/models/graph/bounded_component_spanning_forest.rs @@ -34,7 +34,7 @@ inventory::submit! { /// integer `K`, and a bound `B`, determine whether the vertices can be /// partitioned into at most `K` non-empty sets such that every set induces a /// connected subgraph and the total weight of each set is at most `B`. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct BoundedComponentSpanningForest { /// The underlying graph. graph: G, @@ -46,6 +46,35 @@ pub struct BoundedComponentSpanningForest { max_weight: W::Sum, } +#[derive(Deserialize)] +#[serde(bound( + deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>" +))] +struct BoundedComponentSpanningForestData { + graph: G, + weights: Vec, + max_components: usize, + max_weight: W::Sum, +} + +impl<'de, G, W> Deserialize<'de> for BoundedComponentSpanningForest +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, + W::Sum: Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = BoundedComponentSpanningForestData::::deserialize(deserializer)?; + Self::new( + data.graph, + data.weights, + data.max_components, + data.max_weight, + ) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct BoundedComponentSpanningForestCreateSpec { /// The underlying graph G=(V,E). @@ -64,49 +93,39 @@ impl TryFrom type Error = crate::registry::ConstructionError; fn try_from(spec: BoundedComponentSpanningForestCreateSpec) -> Result { - if spec.weights.len() != spec.graph.num_vertices() { - return Err(format!( - "weights has {} entries, expected {}", - spec.weights.len(), - spec.graph.num_vertices() - ) - .into()); - } - if spec.weights.iter().any(|&weight| weight < 0) { - return Err("weights must be nonnegative".to_string().into()); - } - if spec.k == 0 { - return Err("k must be at least 1".to_string().into()); - } - if spec.max_weight <= 0 { - return Err("max_weight must be positive".to_string().into()); - } - Ok(Self::new(spec.graph, spec.weights, spec.k, spec.max_weight)) + Self::new(spec.graph, spec.weights, spec.k, spec.max_weight) } } impl BoundedComponentSpanningForest { /// Create a new bounded-component spanning forest instance. - pub fn new(graph: G, weights: Vec, max_components: usize, max_weight: W::Sum) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - assert!( - weights - .iter() - .all(|weight| weight.to_sum() >= W::Sum::zero()), - "weights must be nonnegative" - ); - assert!(max_components >= 1, "max_components must be at least 1"); - assert!(max_weight > W::Sum::zero(), "max_weight must be positive"); - Self { + pub fn new( + graph: G, + weights: Vec, + max_components: usize, + max_weight: W::Sum, + ) -> Result { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + if !(weights + .iter() + .all(|weight| weight.to_sum() >= W::Sum::zero())) + { + return Err("weights must be nonnegative".into()); + } + if max_components == 0 { + return Err("max_components must be at least 1".into()); + } + if max_weight.partial_cmp(&W::Sum::zero()) != Some(std::cmp::Ordering::Greater) { + return Err("max_weight must be positive".into()); + } + Ok(Self { graph, weights, max_components, max_weight, - } + }) } /// Get a reference to the underlying graph. @@ -258,8 +277,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.max_components; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.max_components) } } @@ -267,26 +290,30 @@ where pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "bounded_component_spanning_forest_simplegraph", - instance: Box::new(BoundedComponentSpanningForest::new( - SimpleGraph::new( - 8, - vec![ - (0, 1), - (1, 2), - (2, 3), - (3, 4), - (4, 5), - (5, 6), - (6, 7), - (0, 7), - (1, 5), - (2, 6), - ], - ), - vec![2, 3, 1, 2, 3, 1, 2, 1], - 3, - 6, - )), + instance: Box::new( + BoundedComponentSpanningForest::new( + SimpleGraph::new( + 8, + vec![ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), + (5, 6), + (6, 7), + (0, 7), + (1, 5), + (2, 6), + ], + ) + .unwrap(), + vec![2, 3, 1, 2, 3, 1, 2, 1], + 3, + 6, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![0, 0, 1, 1, 1, 2, 2, 0]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/bounded_diameter_spanning_tree.rs b/src/models/graph/bounded_diameter_spanning_tree.rs index e156f4525..f8b1545c4 100644 --- a/src/models/graph/bounded_diameter_spanning_tree.rs +++ b/src/models/graph/bounded_diameter_spanning_tree.rs @@ -52,14 +52,14 @@ inventory::submit! { /// use problemreductions::topology::SimpleGraph; /// use problemreductions::{Problem, BruteForce}; /// -/// let graph = SimpleGraph::new(5, vec![(0,1),(0,2),(0,3),(1,2),(1,4),(2,3),(3,4)]); -/// let problem = BoundedDiameterSpanningTree::new(graph, vec![1,2,1,1,2,1,1], 5, 3); +/// let graph = SimpleGraph::new(5, vec![(0,1),(0,2),(0,3),(1,2),(1,4),(2,3),(3,4)]).unwrap(); +/// let problem = BoundedDiameterSpanningTree::new(graph, vec![1,2,1,1,2,1,1], 5, 3).unwrap(); /// /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound( deserialize = "G: serde::Deserialize<'de>, W: serde::Deserialize<'de>, W::Sum: serde::Deserialize<'de>" ))] @@ -76,6 +76,35 @@ pub struct BoundedDiameterSpanningTree { edge_list: Vec<(usize, usize)>, } +#[derive(Deserialize)] +#[serde(bound( + deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>" +))] +struct BoundedDiameterSpanningTreeData { + graph: G, + edge_weights: Vec, + weight_bound: W::Sum, + diameter_bound: usize, +} + +impl<'de, G, W> Deserialize<'de> for BoundedDiameterSpanningTree +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, + W::Sum: Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = BoundedDiameterSpanningTreeData::::deserialize(deserializer)?; + Self::new( + data.graph, + data.edge_weights, + data.weight_bound, + data.diameter_bound, + ) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct BoundedDiameterSpanningTreeCreateSpec { #[create(codec = "edge-list")] @@ -97,29 +126,7 @@ impl TryFrom let edge_weights = spec .edge_weights .unwrap_or_else(|| vec![1; graph.num_edges()]); - if edge_weights.len() != graph.num_edges() { - return Err(format!( - "edge_weights has length {}, expected {}", - edge_weights.len(), - graph.num_edges() - ) - .into()); - } - if edge_weights.iter().any(|&weight| weight <= 0) { - return Err("edge_weights must be positive".to_string().into()); - } - if spec.weight_bound <= 0 { - return Err("weight_bound must be positive".to_string().into()); - } - if spec.diameter_bound == 0 { - return Err("diameter_bound must be at least 1".to_string().into()); - } - Ok(Self::new( - graph, - edge_weights, - spec.weight_bound, - spec.diameter_bound, - )) + Self::new(graph, edge_weights, spec.weight_bound, spec.diameter_bound) } } @@ -148,41 +155,37 @@ fn simple_graph_from_create( if num_vertices < inferred { return Err(format!("num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}").into()); } - Ok(SimpleGraph::new(num_vertices, edges)) + SimpleGraph::new(num_vertices, edges) } impl BoundedDiameterSpanningTree { /// Create a new Bounded Diameter Spanning Tree instance. /// - /// # Panics - /// Panics if `edge_weights` length does not match the graph's edge count, + /// # Errors + /// Returns an error if `edge_weights` length does not match the graph's edge count, /// if any edge weight is not positive, or if `diameter_bound` is zero. pub fn new( graph: G, edge_weights: Vec, weight_bound: W::Sum, diameter_bound: usize, - ) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); + ) -> Result { + Self::check_weights(&graph, &edge_weights)?; let zero = W::Sum::zero(); - assert!( - edge_weights.iter().all(|w| w.to_sum() > zero.clone()), - "All edge weights must be positive (> 0)" - ); - assert!(weight_bound > zero, "weight_bound must be positive (> 0)"); - assert!(diameter_bound >= 1, "diameter_bound must be at least 1"); + if weight_bound.partial_cmp(&zero) != Some(std::cmp::Ordering::Greater) { + return Err("weight_bound must be positive (> 0)".into()); + } + if diameter_bound == 0 { + return Err("diameter_bound must be at least 1".into()); + } let edge_list = graph.edges(); - Self { + Ok(Self { graph, edge_weights, weight_bound, diameter_bound, edge_list, - } + }) } /// Get a reference to the underlying graph. @@ -196,18 +199,26 @@ impl BoundedDiameterSpanningTree { } /// Set new edge weights. - pub fn set_weights(&mut self, edge_weights: Vec) { - assert_eq!( - edge_weights.len(), - self.graph.num_edges(), - "edge_weights length must match num_edges" - ); - let zero = W::Sum::zero(); - assert!( - edge_weights.iter().all(|w| w.to_sum() > zero.clone()), - "All edge weights must be positive (> 0)" - ); + pub fn set_weights( + &mut self, + edge_weights: Vec, + ) -> Result<(), crate::registry::ConstructionError> { + Self::check_weights(&self.graph, &edge_weights)?; self.edge_weights = edge_weights; + Ok(()) + } + + fn check_weights(graph: &G, weights: &[W]) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_edges() { + return Err("edge_weights length must match num_edges".into()); + } + if !weights + .iter() + .all(|weight| weight.to_sum() > W::Sum::zero()) + { + return Err("edge_weights must be positive (> 0)".into()); + } + Ok(()) } /// Get the weight bound B. @@ -365,8 +376,12 @@ where G: Graph + VariantParam, W: WeightElement + VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.edge_list.len()] + fn num_variables(&self) -> Result { + Ok(self.edge_list.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -386,15 +401,19 @@ pub(crate) fn canonical_model_example_specs() -> Vec { /// The underlying graph. @@ -66,19 +66,38 @@ pub struct DegreeConstrainedSpanningTree { edge_list: Vec<(usize, usize)>, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct DegreeConstrainedSpanningTreeData { + graph: G, + max_degree: usize, +} + +impl<'de, G> Deserialize<'de> for DegreeConstrainedSpanningTree +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = DegreeConstrainedSpanningTreeData::::deserialize(deserializer)?; + Self::new(data.graph, data.max_degree).map_err(serde::de::Error::custom) + } +} + impl DegreeConstrainedSpanningTree { /// Create a new Degree-Constrained Spanning Tree instance. /// - /// # Panics - /// Panics if `max_degree` is zero. - pub fn new(graph: G, max_degree: usize) -> Self { - assert!(max_degree >= 1, "max_degree must be at least 1"); + /// # Errors + /// Returns an error if `max_degree` is zero. + pub fn new(graph: G, max_degree: usize) -> Result { + if max_degree == 0 { + return Err("max_degree must be at least 1".into()); + } let edge_list = graph.edges(); - Self { + Ok(Self { graph, max_degree, edge_list, - } + }) } /// Get a reference to the underlying graph. @@ -191,8 +210,12 @@ impl crate::solvers::BruteForceProblem for DegreeConstrainedSpanningTree where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.edge_list.len()] + fn num_variables(&self) -> Result { + Ok(self.edge_list.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -212,13 +235,17 @@ pub(crate) fn canonical_model_example_specs() -> Vec1->2->3 -/// let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); +/// let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); /// let problem = DirectedHamiltonianPath::new(graph); /// /// let solver = BruteForce::new(); @@ -118,14 +118,13 @@ impl Problem for DirectedHamiltonianPath { } impl crate::solvers::BruteForceProblem for DirectedHamiltonianPath { - fn dimensions(&self) -> Vec { - lehmer_dims(self.graph.num_vertices()) + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) } -} -/// Returns the Lehmer code dimension vector for `n` items: `[n, n-1, ..., 2, 1]`. -pub(crate) fn lehmer_dims(n: usize) -> Vec { - (1..=n).rev().collect() + fn dimension(&self, variable: usize) -> Result { + Ok(self.graph.num_vertices() - variable) + } } /// Decode a Lehmer code into a permutation. @@ -190,7 +189,8 @@ pub(crate) fn canonical_model_example_specs() -> Vec, + source_1: usize, + sink_1: usize, + source_2: usize, + sink_2: usize, + requirement_1: i64, + requirement_2: i64, +} + +impl TryFrom for DirectedTwoCommodityIntegralFlow { + type Error = crate::registry::ConstructionError; + fn try_from(data: DirectedTwoCommodityIntegralFlowData) -> Result { + Self::new( + data.graph, + data.capacities, + data.source_1, + data.sink_1, + data.source_2, + data.sink_2, + data.requirement_1, + data.requirement_2, + ) + } +} + impl DirectedTwoCommodityIntegralFlow { /// Create a new Directed Two-Commodity Integral Flow problem. /// - /// # Panics + /// # Errors /// - /// Panics if: + /// Returns an error if: /// - `capacities.len() != graph.num_arcs()` /// - Any terminal vertex index >= `graph.num_vertices()` #[allow(clippy::too_many_arguments)] @@ -105,26 +134,30 @@ impl DirectedTwoCommodityIntegralFlow { sink_2: usize, requirement_1: i64, requirement_2: i64, - ) -> Self { + ) -> Result { let n = graph.num_vertices(); - assert_eq!( - capacities.len(), - graph.num_arcs(), - "capacities length must match graph num_arcs" - ); - assert!( - capacities.iter().all(|&capacity| capacity >= 0), - "capacities must be nonnegative" - ); - assert!( - requirement_1 >= 0 && requirement_2 >= 0, - "flow requirements must be nonnegative" - ); - assert!(source_1 < n, "source_1 ({source_1}) >= num_vertices ({n})"); - assert!(sink_1 < n, "sink_1 ({sink_1}) >= num_vertices ({n})"); - assert!(source_2 < n, "source_2 ({source_2}) >= num_vertices ({n})"); - assert!(sink_2 < n, "sink_2 ({sink_2}) >= num_vertices ({n})"); - Self { + if capacities.len() != graph.num_arcs() { + return Err("capacities length must match graph num_arcs".into()); + } + if !(capacities.iter().all(|&capacity| capacity >= 0)) { + return Err("capacities must be nonnegative".into()); + } + if !(requirement_1 >= 0 && requirement_2 >= 0) { + return Err("flow requirements must be nonnegative".into()); + } + if !(source_1 < n) { + return Err(format!("source_1 ({source_1}) >= num_vertices ({n})").into()); + } + if !(sink_1 < n) { + return Err(format!("sink_1 ({sink_1}) >= num_vertices ({n})").into()); + } + if !(source_2 < n) { + return Err(format!("source_2 ({source_2}) >= num_vertices ({n})").into()); + } + if !(sink_2 < n) { + return Err(format!("sink_2 ({sink_2}) >= num_vertices ({n})").into()); + } + Ok(Self { graph, capacities, source_1, @@ -133,7 +166,7 @@ impl DirectedTwoCommodityIntegralFlow { sink_2, requirement_1, requirement_2, - } + }) } /// Get a reference to the underlying directed graph. @@ -319,12 +352,14 @@ impl Problem for DirectedTwoCommodityIntegralFlow { } impl crate::solvers::BruteForceProblem for DirectedTwoCommodityIntegralFlow { - fn dimensions(&self) -> Vec { - self.capacities - .iter() - .chain(self.capacities.iter()) - .map(|&c| (c as usize) + 1) - .collect() + fn num_variables(&self) -> Result { + Ok(2 * self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from( + i128::from(self.capacities[variable % self.capacities.len()]) + 1, + )?) } } @@ -340,28 +375,32 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "directed_two_commodity_integral_flow", - instance: Box::new(DirectedTwoCommodityIntegralFlow::new( - DirectedGraph::new( - 6, - vec![ - (0, 2), - (0, 3), - (1, 2), - (1, 3), - (2, 4), - (2, 5), - (3, 4), - (3, 5), - ], - ), - vec![1; 8], - 0, - 4, - 1, - 5, - 1, - 1, - )), + instance: Box::new( + DirectedTwoCommodityIntegralFlow::new( + DirectedGraph::new( + 6, + vec![ + (0, 2), + (0, 3), + (1, 2), + (1, 3), + (2, 4), + (2, 5), + (3, 4), + (3, 5), + ], + ) + .unwrap(), + vec![1; 8], + 0, + 4, + 1, + 5, + 1, + 1, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/disjoint_connecting_paths.rs b/src/models/graph/disjoint_connecting_paths.rs index 551e97e0f..56d34e5ef 100644 --- a/src/models/graph/disjoint_connecting_paths.rs +++ b/src/models/graph/disjoint_connecting_paths.rs @@ -30,13 +30,30 @@ inventory::submit! { /// A configuration uses one binary variable per edge in the graph's canonical /// sorted edge list. A valid solution selects exactly the edges of one simple /// path for each terminal pair, with all such paths pairwise vertex-disjoint. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct DisjointConnectingPaths { graph: G, terminal_pairs: Vec<(usize, usize)>, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct DisjointConnectingPathsData { + graph: G, + terminal_pairs: Vec<(usize, usize)>, +} + +impl<'de, G> Deserialize<'de> for DisjointConnectingPaths +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = DisjointConnectingPathsData::::deserialize(deserializer)?; + Self::new(data.graph, data.terminal_pairs).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct DisjointConnectingPathsCreateSpec { #[create(codec = "edge-list")] @@ -66,68 +83,51 @@ impl TryFrom for DisjointConnectingPaths= count || sink >= count { - return Err("terminal pair endpoint is out of bounds".into()); - } - if source == sink { - return Err("terminal pair endpoints must be distinct".into()); - } - if used[source] || used[sink] { - return Err("terminal vertices must be pairwise disjoint".into()); - } - used[source] = true; - used[sink] = true; - } - Ok(Self { - graph: SimpleGraph::new(count, spec.graph), - terminal_pairs: spec.terminal_pairs, - }) + Self::new(SimpleGraph::new(count, spec.graph)?, spec.terminal_pairs) } } impl DisjointConnectingPaths { /// Create a new Disjoint Connecting Paths instance. /// - /// # Panics + /// # Errors /// - /// Panics if no terminal pairs are provided, if a pair uses invalid or + /// Returns an error if no terminal pairs are provided, if a pair uses invalid or /// repeated endpoints, or if any terminal appears in more than one pair. - pub fn new(graph: G, terminal_pairs: Vec<(usize, usize)>) -> Self { - assert!( - !terminal_pairs.is_empty(), - "terminal_pairs must contain at least one pair" - ); + pub fn new( + graph: G, + terminal_pairs: Vec<(usize, usize)>, + ) -> Result { + if terminal_pairs.is_empty() { + return Err("terminal_pairs must contain at least one pair".into()); + } let num_vertices = graph.num_vertices(); let mut used = vec![false; num_vertices]; for &(source, sink) in &terminal_pairs { - assert!(source < num_vertices, "terminal pair source out of bounds"); - assert!(sink < num_vertices, "terminal pair sink out of bounds"); - assert_ne!(source, sink, "terminal pair endpoints must be distinct"); - assert!( - !used[source], - "terminal vertices must be pairwise disjoint across pairs" - ); - assert!( - !used[sink], - "terminal vertices must be pairwise disjoint across pairs" - ); + if !(source < num_vertices) { + return Err("terminal pair source out of bounds".into()); + } + if !(sink < num_vertices) { + return Err("terminal pair sink out of bounds".into()); + } + if source == sink { + return Err("terminal pair endpoints must be distinct".into()); + } + if !(!used[source]) { + return Err("terminal vertices must be pairwise disjoint across pairs".into()); + } + if !(!used[sink]) { + return Err("terminal vertices must be pairwise disjoint across pairs".into()); + } used[source] = true; used[sink] = true; } - Self { + Ok(Self { graph, terminal_pairs, - } + }) } /// Get a reference to the underlying graph. @@ -201,8 +201,12 @@ impl crate::solvers::BruteForceProblem for DisjointConnectingPaths where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -324,13 +328,17 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "disjoint_connecting_paths_simplegraph", - instance: Box::new(DisjointConnectingPaths::new( - SimpleGraph::new( - 6, - vec![(0, 1), (1, 3), (0, 2), (1, 4), (2, 4), (3, 5), (4, 5)], - ), - vec![(0, 3), (2, 5)], - )), + instance: Box::new( + DisjointConnectingPaths::new( + SimpleGraph::new( + 6, + vec![(0, 1), (1, 3), (0, 2), (1, 4), (2, 4), (3, 5), (4, 5)], + ) + .unwrap(), + vec![(0, 3), (2, 5)], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![true, false, true, false, true, false, true]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/eulerian_path.rs b/src/models/graph/eulerian_path.rs index 661ebd04a..9ca94b23d 100644 --- a/src/models/graph/eulerian_path.rs +++ b/src/models/graph/eulerian_path.rs @@ -44,7 +44,7 @@ inventory::submit! { /// A configuration is an arc-ordering `pi`: position `t` carries the index of /// the arc occurrence used as the `t`-th arc of the trail. /// -/// `dims() = vec![m; m]` where `m = num_arcs()`. A configuration is feasible +/// `coordinate cardinalities = vec![m; m]` where `m = num_arcs()`. A configuration is feasible /// when: /// 1. it is a permutation of `0..m` (all values distinct, each in range), and /// 2. for every consecutive pair `(pi[t], pi[t+1])`, the target vertex of arc @@ -61,7 +61,7 @@ inventory::submit! { /// use problemreductions::{BruteForce, Problem}; /// /// // V = {0,1,2}; A = [(0,1), (0,1), (1,2), (2,0)] (parallel arc (0,1)). -/// let graph = DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)]); +/// let graph = DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)]).unwrap(); /// let problem = EulerianPath::new(graph); /// /// // Witness: ordering [a_0, a_2, a_3, a_1] = (0->1)->(1->2)->(2->0)->(0->1) @@ -135,9 +135,12 @@ impl Problem for EulerianPath { } impl crate::solvers::BruteForceProblem for EulerianPath { - fn dimensions(&self) -> Vec { - let m = self.graph.num_arcs(); - vec![m; m] + fn num_variables(&self) -> Result { + Ok(self.graph.num_arcs()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_arcs()) } } @@ -183,7 +186,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec1->2->0->1. - let graph = DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)]); + let graph = DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)]).unwrap(); let optimal_config = vec![0usize, 2, 3, 1]; vec![crate::example_db::specs::ModelExampleSpec { id: "eulerian_path", diff --git a/src/models/graph/generalized_hex.rs b/src/models/graph/generalized_hex.rs index 7e91bc9b9..ad85c440a 100644 --- a/src/models/graph/generalized_hex.rs +++ b/src/models/graph/generalized_hex.rs @@ -32,7 +32,7 @@ inventory::submit! { /// The problem is represented as a zero-variable decision problem: the graph /// instance fully determines the question, so `evaluate([])` runs a memoized /// game-tree search from the initial empty board. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct GeneralizedHex { graph: G, @@ -40,6 +40,24 @@ pub struct GeneralizedHex { target: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct GeneralizedHexData { + graph: G, + source: usize, + target: usize, +} + +impl<'de, G> Deserialize<'de> for GeneralizedHex +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = GeneralizedHexData::::deserialize(deserializer)?; + Self::new(data.graph, data.source, data.target).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct GeneralizedHexCreateSpec { /// The underlying graph G=(V,E). @@ -54,25 +72,7 @@ impl TryFrom for GeneralizedHex { type Error = crate::registry::ConstructionError; fn try_from(spec: GeneralizedHexCreateSpec) -> Result { - let num_vertices = spec.graph.num_vertices(); - if spec.source >= num_vertices { - return Err(format!( - "source {} is outside graph with {num_vertices} vertices", - spec.source - ) - .into()); - } - if spec.sink >= num_vertices { - return Err(format!( - "sink {} is outside graph with {num_vertices} vertices", - spec.sink - ) - .into()); - } - if spec.source == spec.sink { - return Err("source and sink must be distinct".to_string().into()); - } - Ok(Self::new(spec.graph, spec.source, spec.sink)) + Self::new(spec.graph, spec.source, spec.sink) } } @@ -85,16 +85,26 @@ enum ClaimState { impl GeneralizedHex { /// Create a new Generalized Hex instance. - pub fn new(graph: G, source: usize, target: usize) -> Self { + pub fn new( + graph: G, + source: usize, + target: usize, + ) -> Result { let num_vertices = graph.num_vertices(); - assert!(source < num_vertices, "source must be a valid graph vertex"); - assert!(target < num_vertices, "target must be a valid graph vertex"); - assert_ne!(source, target, "source and target must be distinct"); - Self { + if !(source < num_vertices) { + return Err("source must be a valid graph vertex".into()); + } + if !(target < num_vertices) { + return Err("target must be a valid graph vertex".into()); + } + if source == target { + return Err("source and target must be distinct".into()); + } + Ok(Self { graph, source, target, - } + }) } /// Get a reference to the underlying graph. @@ -305,8 +315,12 @@ impl crate::solvers::BruteForceProblem for GeneralizedHex where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![] + fn num_variables(&self) -> Result { + Ok(0) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(0) } } @@ -315,7 +329,7 @@ crate::impl_random_generate!( crate::random::EndpointRandomSpec, |spec| { let (source, sink) = spec.endpoints()?; - Ok(GeneralizedHex::new(spec.graph()?, source, sink)) + GeneralizedHex::new(spec.graph()?, source, sink) } ); @@ -331,14 +345,18 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "generalized_hex_simplegraph", - instance: Box::new(GeneralizedHex::new( - SimpleGraph::new( - 6, - vec![(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4), (4, 5)], - ), - 0, - 5, - )), + instance: Box::new( + GeneralizedHex::new( + SimpleGraph::new( + 6, + vec![(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4), (4, 5)], + ) + .unwrap(), + 0, + 5, + ) + .unwrap(), + ), optimal_config: serde_json::json!(null), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/graph_partitioning.rs b/src/models/graph/graph_partitioning.rs index 6a59a6e48..e064966ff 100644 --- a/src/models/graph/graph_partitioning.rs +++ b/src/models/graph/graph_partitioning.rs @@ -45,7 +45,7 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Square graph: 0-1, 1-2, 2-3, 3-0 -/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]); +/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(); /// let problem = GraphPartitioning::new(graph); /// /// let solver = BruteForce::new(); @@ -138,8 +138,12 @@ impl crate::solvers::BruteForceProblem for GraphPartitioning where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -157,20 +161,23 @@ pub(crate) fn canonical_model_example_specs() -> Vec crate::solvers::BruteForceProblem for HamiltonianCircuit where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } @@ -171,20 +174,23 @@ pub(crate) fn canonical_model_example_specs() -> Vec crate::solvers::BruteForceProblem for HamiltonianPath where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } @@ -174,19 +177,22 @@ pub(crate) fn is_valid_hamiltonian_path(graph: &G, config: &[usize]) - pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "hamiltonian_path_simplegraph", - instance: Box::new(HamiltonianPath::new(SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (1, 3), - (2, 3), - (3, 4), - (3, 5), - (4, 2), - (5, 1), - ], - ))), + instance: Box::new(HamiltonianPath::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 3), + (2, 3), + (3, 4), + (3, 5), + (4, 2), + (5, 1), + ], + ) + .unwrap(), + )), optimal_config: serde_json::json!(vec![0, 2, 4, 3, 1, 5]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/hamiltonian_path_between_two_vertices.rs b/src/models/graph/hamiltonian_path_between_two_vertices.rs index 7ae5bbbcd..9a9537d23 100644 --- a/src/models/graph/hamiltonian_path_between_two_vertices.rs +++ b/src/models/graph/hamiltonian_path_between_two_vertices.rs @@ -46,7 +46,7 @@ inventory::submit! { /// - The last element equals `target_vertex` /// - Consecutive entries are adjacent in the graph /// -/// The search space has `dims() = [n; n]` (each position can hold any of `n` +/// The search space has `coordinate cardinalities = [n; n]` (each position can hold any of `n` /// vertices), so brute-force enumerates `n^n` configurations. /// /// # Type Parameters @@ -61,14 +61,14 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Path graph: 0-1-2-3, source=0, target=3 -/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); -/// let problem = HamiltonianPathBetweenTwoVertices::new(graph, 0, 3); +/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); +/// let problem = HamiltonianPathBetweenTwoVertices::new(graph, 0, 3).unwrap(); /// /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct HamiltonianPathBetweenTwoVertices { graph: G, @@ -76,6 +76,25 @@ pub struct HamiltonianPathBetweenTwoVertices { target_vertex: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct HamiltonianPathBetweenTwoVerticesData { + graph: G, + source_vertex: usize, + target_vertex: usize, +} + +impl<'de, G> Deserialize<'de> for HamiltonianPathBetweenTwoVertices +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = HamiltonianPathBetweenTwoVerticesData::::deserialize(deserializer)?; + Self::new(data.graph, data.source_vertex, data.target_vertex) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct HamiltonianPathBetweenTwoVerticesRandomSpec { /// Number of graph vertices. @@ -93,28 +112,35 @@ struct HamiltonianPathBetweenTwoVerticesRandomSpec { impl HamiltonianPathBetweenTwoVertices { /// Create a new Hamiltonian Path Between Two Vertices problem. /// - /// # Panics + /// # Errors /// - /// Panics if `source_vertex` or `target_vertex` is out of range, or if they are equal. - pub fn new(graph: G, source_vertex: usize, target_vertex: usize) -> Self { + /// Returns an error if `source_vertex` or `target_vertex` is out of range, or if they are equal. + pub fn new( + graph: G, + source_vertex: usize, + target_vertex: usize, + ) -> Result { let n = graph.num_vertices(); - assert!( - source_vertex < n, - "source_vertex {source_vertex} out of range for graph with {n} vertices" - ); - assert!( - target_vertex < n, - "target_vertex {target_vertex} out of range for graph with {n} vertices" - ); - assert_ne!( - source_vertex, target_vertex, - "source_vertex and target_vertex must be distinct" - ); - Self { + if !(source_vertex < n) { + return Err(format!( + "source_vertex {source_vertex} out of range for graph with {n} vertices" + ) + .into()); + } + if !(target_vertex < n) { + return Err(format!( + "target_vertex {target_vertex} out of range for graph with {n} vertices" + ) + .into()); + } + if source_vertex == target_vertex { + return Err("source_vertex and target_vertex must be distinct".into()); + } + Ok(Self { graph, source_vertex, target_vertex, - } + }) } /// Get a reference to the underlying graph. @@ -192,9 +218,12 @@ impl crate::solvers::BruteForceProblem for HamiltonianPathBetweenTwoVertices< where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } @@ -245,23 +274,27 @@ pub(crate) fn canonical_model_example_specs() -> Vec crate::solvers::BruteForceProblem for HighlyConnectedDeletion where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -335,10 +339,9 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "highly_connected_deletion_simplegraph", - instance: Box::new(HighlyConnectedDeletion::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (1, 2), (2, 3)], - ))), + instance: Box::new(HighlyConnectedDeletion::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]).unwrap(), + )), // Edges in input order; deleting only edge index 3 = (2,3) leaves K3 + {3}. optimal_config: serde_json::json!(vec![false, false, false, true]), optimal_value: serde_json::json!(1), diff --git a/src/models/graph/integral_flow_bundles.rs b/src/models/graph/integral_flow_bundles.rs index a9f99f931..81219f4fe 100644 --- a/src/models/graph/integral_flow_bundles.rs +++ b/src/models/graph/integral_flow_bundles.rs @@ -24,6 +24,7 @@ inventory::submit! { /// Integral Flow with Bundles (Garey & Johnson ND36). #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "IntegralFlowBundlesData")] pub struct IntegralFlowBundles { graph: DirectedGraph, source: usize, @@ -33,6 +34,30 @@ pub struct IntegralFlowBundles { requirement: i64, } +#[derive(Deserialize)] +struct IntegralFlowBundlesData { + graph: DirectedGraph, + source: usize, + sink: usize, + bundles: Vec>, + bundle_capacities: Vec, + requirement: i64, +} + +impl TryFrom for IntegralFlowBundles { + type Error = crate::registry::ConstructionError; + fn try_from(data: IntegralFlowBundlesData) -> Result { + Self::new( + data.graph, + data.source, + data.sink, + data.bundles, + data.bundle_capacities, + data.requirement, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct IntegralFlowBundlesCreateSpec { #[create(codec = "arc-list")] @@ -52,9 +77,6 @@ impl TryFrom for IntegralFlowBundles { fn try_from( spec: IntegralFlowBundlesCreateSpec, ) -> Result { - if spec.arcs.is_empty() { - return Err("arcs must be non-empty".into()); - } let inferred = spec .arcs .iter() @@ -64,60 +86,14 @@ impl TryFrom for IntegralFlowBundles { .transpose()? .unwrap_or(0); let count = spec.num_vertices.unwrap_or(inferred); - if count < inferred { - return Err("num_vertices is too small".into()); - } - if spec.source >= count || spec.sink >= count { - return Err("source and sink must be valid vertices".into()); - } - if spec.source == spec.sink { - return Err("source and sink must be distinct".into()); - } - if spec.bundles.len() != spec.bundle_capacities.len() { - return Err("bundles length must match bundle_capacities length".into()); - } - if spec.requirement == 0 { - return Err("requirement must be positive".into()); - } - let mut covered = vec![false; spec.arcs.len()]; - let mut upper = vec![i64::MAX; spec.arcs.len()]; - for (i, (bundle, &capacity)) in spec.bundles.iter().zip(&spec.bundle_capacities).enumerate() - { - if capacity == 0 { - return Err(format!("bundle capacity {i} must be positive").into()); - } - let mut seen = BTreeSet::new(); - for &arc in bundle { - if arc >= spec.arcs.len() { - return Err(format!("bundle {i} arc is out of range").into()); - } - if !seen.insert(arc) { - return Err(format!("bundle {i} contains duplicate arc").into()); - } - covered[arc] = true; - upper[arc] = upper[arc].min(capacity); - } - } - for (arc, &is_covered) in covered.iter().enumerate() { - if !is_covered { - return Err(format!("arc {arc} must belong to a bundle").into()); - } - if usize::try_from(upper[arc]) - .ok() - .and_then(|v| v.checked_add(1)) - .is_none() - { - return Err(format!("arc {arc} upper bound is too large").into()); - } - } - Ok(Self { - graph: DirectedGraph::new(count, spec.arcs), - source: spec.source, - sink: spec.sink, - bundles: spec.bundles, - bundle_capacities: spec.bundle_capacities, - requirement: spec.requirement, - }) + Self::new( + DirectedGraph::new(count, spec.arcs)?, + spec.source, + spec.sink, + spec.bundles, + spec.bundle_capacities, + spec.requirement, + ) } } @@ -130,74 +106,66 @@ impl IntegralFlowBundles { bundles: Vec>, bundle_capacities: Vec, requirement: i64, - ) -> Self { + ) -> Result { let num_vertices = graph.num_vertices(); let num_arcs = graph.num_arcs(); - assert!( - source < num_vertices, - "source ({source}) >= num_vertices ({num_vertices})" - ); - assert!( - sink < num_vertices, - "sink ({sink}) >= num_vertices ({num_vertices})" - ); - assert!(source != sink, "source and sink must be distinct"); - assert_eq!( - bundles.len(), - bundle_capacities.len(), - "bundles length must match bundle_capacities length" - ); - assert!(requirement > 0, "requirement must be positive"); + if !(source < num_vertices) { + return Err(format!("source ({source}) >= num_vertices ({num_vertices})").into()); + } + if !(sink < num_vertices) { + return Err(format!("sink ({sink}) >= num_vertices ({num_vertices})").into()); + } + if source == sink { + return Err("source and sink must be distinct".into()); + } + if bundles.len() != bundle_capacities.len() { + return Err("bundles length must match bundle_capacities length".into()); + } + if requirement <= 0 { + return Err("requirement must be positive".into()); + } let mut arc_covered = vec![false; num_arcs]; - let mut arc_upper_bounds = vec![i64::MAX; num_arcs]; for (bundle_index, (bundle, &capacity)) in bundles.iter().zip(&bundle_capacities).enumerate() { - assert!( - capacity > 0, - "bundle capacity at index {bundle_index} must be positive" - ); + if !(capacity > 0) { + return Err( + format!("bundle capacity at index {bundle_index} must be positive").into(), + ); + } let mut seen = BTreeSet::new(); for &arc_index in bundle { - assert!( - arc_index < num_arcs, - "bundle {bundle_index} references arc {arc_index}, but num_arcs is {num_arcs}" - ); - assert!( - seen.insert(arc_index), - "bundle {bundle_index} contains duplicate arc index {arc_index}" - ); + if !(arc_index < num_arcs) { + return Err(format!("bundle {bundle_index} references arc {arc_index}, but num_arcs is {num_arcs}").into()); + } + if !(seen.insert(arc_index)) { + return Err(format!( + "bundle {bundle_index} contains duplicate arc index {arc_index}" + ) + .into()); + } arc_covered[arc_index] = true; - arc_upper_bounds[arc_index] = arc_upper_bounds[arc_index].min(capacity); } } for (arc_index, covered) in arc_covered.iter().copied().enumerate() { - assert!( - covered, - "arc {arc_index} must belong to at least one bundle" - ); - let domain = usize::try_from(arc_upper_bounds[arc_index]) - .ok() - .and_then(|bound| bound.checked_add(1)); - assert!( - domain.is_some(), - "bundle-derived upper bound for arc {arc_index} must fit into usize for dims()" - ); + if !(covered) { + return Err(format!("arc {arc_index} must belong to at least one bundle").into()); + } } - Self { + Ok(Self { graph, source, sink, bundles, bundle_capacities, requirement, - } + }) } /// Get the underlying directed graph. @@ -369,16 +337,20 @@ impl Problem for IntegralFlowBundles { } impl crate::solvers::BruteForceProblem for IntegralFlowBundles { - fn dimensions(&self) -> Vec { - self.arc_upper_bounds() - .into_iter() - .map(|bound| { - usize::try_from(bound) - .ok() - .and_then(|bound| bound.checked_add(1)) - .expect("bundle-derived arc upper bounds are validated in the constructor") - }) - .collect() + fn num_variables(&self) -> Result { + Ok(self.num_arcs()) + } + + fn dimension(&self, variable: usize) -> Result { + let bound = self + .bundles + .iter() + .zip(&self.bundle_capacities) + .filter(|(bundle, _)| bundle.contains(&variable)) + .map(|(_, &capacity)| capacity) + .min() + .expect("each arc belongs to at least one bundle"); + Ok(usize::try_from(i128::from(bound) + 1)?) } } @@ -394,14 +366,18 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "integral_flow_bundles", - instance: Box::new(IntegralFlowBundles::new( - DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]), - 0, - 3, - vec![vec![0, 1], vec![2, 5], vec![3, 4]], - vec![1, 1, 1], - 1, - )), + instance: Box::new( + IntegralFlowBundles::new( + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]) + .unwrap(), + 0, + 3, + vec![vec![0, 1], vec![2, 5], vec![3, 4]], + vec![1, 1, 1], + 1, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![1, 0, 1, 0, 0, 0]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/integral_flow_homologous_arcs.rs b/src/models/graph/integral_flow_homologous_arcs.rs index 0ea45a9e2..aad79bf0f 100644 --- a/src/models/graph/integral_flow_homologous_arcs.rs +++ b/src/models/graph/integral_flow_homologous_arcs.rs @@ -29,6 +29,7 @@ inventory::submit! { /// capacities, flow conservation at non-terminal vertices, every homologous-pair /// equality constraint, and the required net inflow at the sink. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "IntegralFlowHomologousArcsData")] pub struct IntegralFlowHomologousArcs { graph: DirectedGraph, capacities: Vec, @@ -38,6 +39,30 @@ pub struct IntegralFlowHomologousArcs { homologous_pairs: Vec<(usize, usize)>, } +#[derive(Deserialize)] +struct IntegralFlowHomologousArcsData { + graph: DirectedGraph, + capacities: Vec, + source: usize, + sink: usize, + requirement: i64, + homologous_pairs: Vec<(usize, usize)>, +} + +impl TryFrom for IntegralFlowHomologousArcs { + type Error = crate::registry::ConstructionError; + fn try_from(data: IntegralFlowHomologousArcsData) -> Result { + Self::new( + data.graph, + data.capacities, + data.source, + data.sink, + data.requirement, + data.homologous_pairs, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct IntegralFlowHomologousArcsCreateSpec { #[create(codec = "arc-list")] @@ -57,9 +82,6 @@ impl TryFrom for IntegralFlowHomologousArc fn try_from( spec: IntegralFlowHomologousArcsCreateSpec, ) -> Result { - if spec.arcs.is_empty() { - return Err("arcs must be non-empty".into()); - } let inferred = spec .arcs .iter() @@ -69,38 +91,15 @@ impl TryFrom for IntegralFlowHomologousArc .transpose()? .unwrap_or(0); let count = spec.num_vertices.unwrap_or(inferred); - if count < inferred { - return Err("num_vertices is too small".into()); - } let capacities = spec.capacities.unwrap_or_else(|| vec![1; spec.arcs.len()]); - if capacities.len() != spec.arcs.len() { - return Err("capacities length must match arcs length".into()); - } - if spec.source >= count || spec.sink >= count { - return Err("source and sink must be valid vertices".into()); - } - for &(a, b) in &spec.homologous_pairs { - if a >= spec.arcs.len() || b >= spec.arcs.len() { - return Err("homologous pair arc index is out of range".into()); - } - } - for &c in &capacities { - if usize::try_from(c) - .ok() - .and_then(|v| v.checked_add(1)) - .is_none() - { - return Err("capacity is too large".into()); - } - } - Ok(Self { - graph: DirectedGraph::new(count, spec.arcs), + Self::new( + DirectedGraph::new(count, spec.arcs)?, capacities, - source: spec.source, - sink: spec.sink, - requirement: spec.requirement, - homologous_pairs: spec.homologous_pairs, - }) + spec.source, + spec.sink, + spec.requirement, + spec.homologous_pairs, + ) } } @@ -112,47 +111,46 @@ impl IntegralFlowHomologousArcs { sink: usize, requirement: i64, homologous_pairs: Vec<(usize, usize)>, - ) -> Self { + ) -> Result { let num_vertices = graph.num_vertices(); let num_arcs = graph.num_arcs(); - assert_eq!( - capacities.len(), - num_arcs, - "capacities length must match graph.num_arcs()" - ); - assert!( - source < num_vertices, - "source ({source}) must be less than num_vertices ({num_vertices})" - ); - assert!( - sink < num_vertices, - "sink ({sink}) must be less than num_vertices ({num_vertices})" - ); + if capacities.len() != num_arcs { + return Err("capacities length must match graph.num_arcs()".into()); + } + if !(source < num_vertices) { + return Err(format!( + "source ({source}) must be less than num_vertices ({num_vertices})" + ) + .into()); + } + if !(sink < num_vertices) { + return Err( + format!("sink ({sink}) must be less than num_vertices ({num_vertices})").into(), + ); + } for &(a, b) in &homologous_pairs { - assert!(a < num_arcs, "homologous arc index {a} out of range"); - assert!(b < num_arcs, "homologous arc index {b} out of range"); + if !(a < num_arcs) { + return Err(format!("homologous arc index {a} out of range").into()); + } + if !(b < num_arcs) { + return Err(format!("homologous arc index {b} out of range").into()); + } } - for &capacity in &capacities { - assert!( - usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)) - .is_some(), - "capacities must fit into usize for dims()" - ); + if !(capacities.iter().all(|&capacity| capacity >= 0)) { + return Err("capacities must be nonnegative".into()); } - Self { + Ok(Self { graph, capacities, source, sink, requirement, homologous_pairs, - } + }) } pub fn graph(&self) -> &DirectedGraph { @@ -248,13 +246,6 @@ impl IntegralFlowHomologousArcs { Ok(crate::types::Or(balances[self.sink] >= self.requirement)) } - - fn domain_size(capacity: i64) -> usize { - usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)) - .expect("capacity already validated to fit into usize") - } } impl Problem for IntegralFlowHomologousArcs { @@ -281,11 +272,12 @@ impl Problem for IntegralFlowHomologousArcs { } impl crate::solvers::BruteForceProblem for IntegralFlowHomologousArcs { - fn dimensions(&self) -> Vec { - self.capacities - .iter() - .map(|&capacity| Self::domain_size(capacity)) - .collect() + fn num_variables(&self) -> Result { + Ok(self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.capacities[variable]) + 1)?) } } @@ -301,26 +293,30 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "integral_flow_homologous_arcs", - instance: Box::new(IntegralFlowHomologousArcs::new( - DirectedGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (1, 3), - (2, 3), - (1, 4), - (2, 4), - (3, 5), - (4, 5), - ], - ), - vec![1; 8], - 0, - 5, - 2, - vec![(2, 5), (4, 3)], - )), + instance: Box::new( + IntegralFlowHomologousArcs::new( + DirectedGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 3), + (2, 3), + (1, 4), + (2, 4), + (3, 5), + (4, 5), + ], + ) + .unwrap(), + vec![1; 8], + 0, + 5, + 2, + vec![(2, 5), (4, 3)], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![1, 1, 1, 0, 0, 1, 1, 1]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/integral_flow_with_multipliers.rs b/src/models/graph/integral_flow_with_multipliers.rs index d5730c110..6fc55b896 100644 --- a/src/models/graph/integral_flow_with_multipliers.rs +++ b/src/models/graph/integral_flow_with_multipliers.rs @@ -23,6 +23,7 @@ inventory::submit! { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "IntegralFlowWithMultipliersData")] pub struct IntegralFlowWithMultipliers { graph: DirectedGraph, source: usize, @@ -32,6 +33,30 @@ pub struct IntegralFlowWithMultipliers { requirement: i64, } +#[derive(Deserialize)] +struct IntegralFlowWithMultipliersData { + graph: DirectedGraph, + source: usize, + sink: usize, + multipliers: Vec, + capacities: Vec, + requirement: i64, +} + +impl TryFrom for IntegralFlowWithMultipliers { + type Error = crate::registry::ConstructionError; + fn try_from(data: IntegralFlowWithMultipliersData) -> Result { + Self::new( + data.graph, + data.source, + data.sink, + data.multipliers, + data.capacities, + data.requirement, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct IntegralFlowWithMultipliersCreateSpec { #[create(codec = "arc-list")] @@ -51,9 +76,6 @@ impl TryFrom for IntegralFlowWithMultipli fn try_from( spec: IntegralFlowWithMultipliersCreateSpec, ) -> Result { - if spec.arcs.is_empty() { - return Err("arcs must be non-empty".into()); - } let inferred = spec .arcs .iter() @@ -63,43 +85,14 @@ impl TryFrom for IntegralFlowWithMultipli .transpose()? .unwrap_or(0); let count = spec.num_vertices.unwrap_or(inferred); - if count < inferred { - return Err("num_vertices is too small".into()); - } - if spec.capacities.len() != spec.arcs.len() { - return Err("capacities length must match arcs length".into()); - } - if spec.multipliers.len() != count { - return Err("multipliers length must match num_vertices".into()); - } - if spec.source >= count || spec.sink >= count { - return Err("source and sink must be valid vertices".into()); - } - if spec.source == spec.sink { - return Err("source and sink must be distinct".into()); - } - for (v, &m) in spec.multipliers.iter().enumerate() { - if v != spec.source && v != spec.sink && m == 0 { - return Err("non-terminal multipliers must be positive".into()); - } - } - for &c in &spec.capacities { - if usize::try_from(c) - .ok() - .and_then(|v| v.checked_add(1)) - .is_none() - { - return Err("capacity is too large".into()); - } - } - Ok(Self { - graph: DirectedGraph::new(count, spec.arcs), - source: spec.source, - sink: spec.sink, - multipliers: spec.multipliers, - capacities: spec.capacities, - requirement: spec.requirement, - }) + Self::new( + DirectedGraph::new(count, spec.arcs)?, + spec.source, + spec.sink, + spec.multipliers, + spec.capacities, + spec.requirement, + ) } } @@ -111,53 +104,48 @@ impl IntegralFlowWithMultipliers { multipliers: Vec, capacities: Vec, requirement: i64, - ) -> Self { - assert_eq!( - capacities.len(), - graph.num_arcs(), - "capacities length must match graph num_arcs" - ); - assert_eq!( - multipliers.len(), - graph.num_vertices(), - "multipliers length must match graph num_vertices" - ); + ) -> Result { + if capacities.len() != graph.num_arcs() { + return Err("capacities length must match graph num_arcs".into()); + } + if multipliers.len() != graph.num_vertices() { + return Err("multipliers length must match graph num_vertices".into()); + } let num_vertices = graph.num_vertices(); - assert!( - source < num_vertices, - "source ({source}) must be less than num_vertices ({num_vertices})" - ); - assert!( - sink < num_vertices, - "sink ({sink}) must be less than num_vertices ({num_vertices})" - ); - assert_ne!(source, sink, "source and sink must be distinct"); + if !(source < num_vertices) { + return Err(format!( + "source ({source}) must be less than num_vertices ({num_vertices})" + ) + .into()); + } + if !(sink < num_vertices) { + return Err( + format!("sink ({sink}) must be less than num_vertices ({num_vertices})").into(), + ); + } + if source == sink { + return Err("source and sink must be distinct".into()); + } for (vertex, &multiplier) in multipliers.iter().enumerate() { - if vertex != source && vertex != sink { - assert!(multiplier > 0, "non-terminal multipliers must be positive"); + if vertex != source && vertex != sink && !(multiplier > 0) { + return Err("non-terminal multipliers must be positive".into()); } } - for &capacity in &capacities { - let domain = usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)); - assert!( - domain.is_some(), - "arc capacities must fit into usize for dims()" - ); + if !(capacities.iter().all(|&capacity| capacity >= 0)) { + return Err("capacities must be nonnegative".into()); } - Self { + Ok(Self { graph, source, sink, multipliers, capacities, requirement, - } + }) } pub fn graph(&self) -> &DirectedGraph { @@ -196,13 +184,6 @@ impl IntegralFlowWithMultipliers { self.capacities.iter().copied().max().unwrap_or(0) } - fn domain_size(capacity: i64) -> usize { - usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)) - .expect("capacity already validated to fit into usize") - } - pub fn is_feasible(&self, config: &[usize]) -> Result { if config.len() != self.num_arcs() { return Ok(false); @@ -297,11 +278,12 @@ impl Problem for IntegralFlowWithMultipliers { } impl crate::solvers::BruteForceProblem for IntegralFlowWithMultipliers { - fn dimensions(&self) -> Vec { - self.capacities - .iter() - .map(|&capacity| Self::domain_size(capacity)) - .collect() + fn num_variables(&self) -> Result { + Ok(self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.capacities[variable]) + 1)?) } } @@ -317,30 +299,34 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "integral_flow_with_multipliers", - instance: Box::new(IntegralFlowWithMultipliers::new( - DirectedGraph::new( - 8, - vec![ - (0, 1), - (0, 2), - (0, 3), - (0, 4), - (0, 5), - (0, 6), - (1, 7), - (2, 7), - (3, 7), - (4, 7), - (5, 7), - (6, 7), - ], - ), - 0, - 7, - vec![1, 2, 3, 4, 5, 6, 4, 1], - vec![1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4], - 12, - )), + instance: Box::new( + IntegralFlowWithMultipliers::new( + DirectedGraph::new( + 8, + vec![ + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (0, 5), + (0, 6), + (1, 7), + (2, 7), + (3, 7), + (4, 7), + (5, 7), + (6, 7), + ], + ) + .unwrap(), + 0, + 7, + vec![1, 2, 3, 4, 5, 6, 4, 1], + vec![1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4], + 12, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![1, 0, 1, 0, 1, 0, 2, 0, 4, 0, 6, 0]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/isomorphic_spanning_tree.rs b/src/models/graph/isomorphic_spanning_tree.rs index ad2877684..8e4bcfee0 100644 --- a/src/models/graph/isomorphic_spanning_tree.rs +++ b/src/models/graph/isomorphic_spanning_tree.rs @@ -45,9 +45,9 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Host graph: triangle 0-1-2-0 -/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); +/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); /// // Tree: path 0-1-2 -/// let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); +/// let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); /// let problem = IsomorphicSpanningTree::new(graph, tree); /// /// let solver = BruteForce::new(); @@ -172,8 +172,12 @@ impl crate::solvers::BruteForceProblem for IsomorphicSpanningTree where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.graph.num_vertices(); self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } @@ -230,8 +234,8 @@ pub(crate) fn canonical_model_example_specs() -> Vec { graph: G, k: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct KCliqueData { + graph: G, + k: usize, +} + +impl<'de, G> Deserialize<'de> for KClique +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = KCliqueData::::deserialize(deserializer)?; + Self::new(data.graph, data.k).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct KCliqueCreateSpec { #[create(codec = "edge-list")] @@ -60,28 +77,20 @@ impl TryFrom for KClique { .transpose()? .unwrap_or(0); let count = spec.num_vertices.unwrap_or(inferred); - if count < inferred { - return Err("num_vertices is too small for graph endpoints".into()); - } - if spec.k == 0 { - return Err("k must be positive".into()); - } - if spec.k > count { - return Err("k must be <= graph num_vertices".into()); - } - Ok(Self { - graph: SimpleGraph::new(count, spec.graph), - k: spec.k, - }) + Self::new(SimpleGraph::new(count, spec.graph)?, spec.k) } } impl KClique { /// Create a new k-Clique problem instance. - pub fn new(graph: G, k: usize) -> Self { - assert!(k > 0, "k must be positive"); - assert!(k <= graph.num_vertices(), "k must be <= graph num_vertices"); - Self { graph, k } + pub fn new(graph: G, k: usize) -> Result { + if k == 0 { + return Err("k must be positive".into()); + } + if !(k <= graph.num_vertices()) { + return Err("k must be <= graph num_vertices".into()); + } + Ok(Self { graph, k }) } /// Get a reference to the underlying graph. @@ -163,8 +172,12 @@ impl crate::solvers::BruteForceProblem for KClique where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -204,7 +217,7 @@ crate::impl_random_generate!( ) .into()); } - Ok(KClique::new(spec.graph()?, spec.k)) + KClique::new(spec.graph()?, spec.k) } ); @@ -220,10 +233,13 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "kclique_simplegraph", - instance: Box::new(KClique::new( - SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]), - 3, - )), + instance: Box::new( + KClique::new( + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(), + 3, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![false, false, true, true, true]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/kcoloring.rs b/src/models/graph/kcoloring.rs index 9f1662123..d0d1b92d0 100644 --- a/src/models/graph/kcoloring.rs +++ b/src/models/graph/kcoloring.rs @@ -44,7 +44,7 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Triangle graph needs at least 3 colors -/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); +/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); /// let problem = KColoring::::new(graph); /// /// let solver = BruteForce::new(); @@ -118,7 +118,7 @@ fn simple_graph_from_create( ) .into()); } - Ok(SimpleGraph::new(count, edges)) + SimpleGraph::new(count, edges) } impl TryFrom for KColoring { @@ -273,8 +273,12 @@ impl crate::solvers::BruteForceProblem for KColoring where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.num_colors; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_colors) } } @@ -309,10 +313,9 @@ pub(crate) fn is_valid_coloring( pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "kcoloring_k3_simplegraph", - instance: Box::new(KColoring::::new(SimpleGraph::new( - 5, - vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)], - ))), + instance: Box::new(KColoring::::new( + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(), + )), optimal_config: serde_json::json!(vec![0, 1, 1, 0, 2]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/kernel.rs b/src/models/graph/kernel.rs index 359c5ad42..66b1827f2 100644 --- a/src/models/graph/kernel.rs +++ b/src/models/graph/kernel.rs @@ -47,7 +47,7 @@ inventory::submit! { /// /// let graph = DirectedGraph::new(5, vec![ /// (0,1),(0,2),(1,3),(2,3),(3,4),(4,0),(4,1), -/// ]); +/// ]).unwrap(); /// let problem = Kernel::new(graph); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); @@ -136,8 +136,12 @@ impl Problem for Kernel { } impl crate::solvers::BruteForceProblem for Kernel { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -148,7 +152,8 @@ pub(crate) fn canonical_model_example_specs() -> Vec { graph: SimpleGraph, weights: Vec, @@ -42,6 +42,26 @@ pub struct KthBestSpanningTree { bound: W::Sum, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>"))] +struct KthBestSpanningTreeData { + graph: SimpleGraph, + weights: Vec, + k: usize, + bound: W::Sum, +} + +impl<'de, W> Deserialize<'de> for KthBestSpanningTree +where + W: WeightElement + Deserialize<'de>, + W::Sum: Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = KthBestSpanningTreeData::::deserialize(deserializer)?; + Self::new(data.graph, data.weights, data.k, data.bound).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct KthBestSpanningTreeCreateSpec { #[create(codec = "edge-list")] @@ -61,18 +81,7 @@ impl TryFrom for KthBestSpanningTree { let weights = spec .edge_weights .unwrap_or_else(|| vec![1; graph.num_edges()]); - if weights.len() != graph.num_edges() { - return Err(format!( - "edge_weights has length {}, expected {}", - weights.len(), - graph.num_edges() - ) - .into()); - } - if spec.k == 0 { - return Err("k must be positive".to_string().into()); - } - Ok(Self::new(graph, weights, spec.k, spec.bound)) + Self::new(graph, weights, spec.k, spec.bound) } } @@ -101,30 +110,35 @@ fn simple_graph_from_create( if num_vertices < inferred { return Err(format!("num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}").into()); } - Ok(SimpleGraph::new(num_vertices, edges)) + SimpleGraph::new(num_vertices, edges) } impl KthBestSpanningTree { /// Create a new KthBestSpanningTree instance. /// - /// # Panics + /// # Errors /// - /// Panics if the number of weights does not match the number of edges, or + /// Returns an error if the number of weights does not match the number of edges, or /// if `k` is zero. - pub fn new(graph: SimpleGraph, weights: Vec, k: usize, bound: W::Sum) -> Self { - assert_eq!( - weights.len(), - graph.num_edges(), - "weights length must match graph num_edges" - ); - assert!(k > 0, "k must be positive"); - - Self { + pub fn new( + graph: SimpleGraph, + weights: Vec, + k: usize, + bound: W::Sum, + ) -> Result { + if weights.len() != graph.num_edges() { + return Err("weights length must match graph num_edges".into()); + } + if k == 0 { + return Err("k must be positive".into()); + } + + Ok(Self { graph, weights, k, bound, - } + }) } /// Get the underlying graph. @@ -303,8 +317,14 @@ impl crate::solvers::BruteForceProblem for KthBestSpanningTree where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.k * self.graph.num_edges()] + fn num_variables(&self) -> Result { + (self.k).checked_mul(self.graph.num_edges()).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -315,8 +335,8 @@ pub(crate) fn canonical_model_example_specs() -> Vec { graph: G, @@ -43,6 +43,26 @@ pub struct LengthBoundedDisjointPaths { max_length: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct LengthBoundedDisjointPathsData { + graph: G, + source: usize, + sink: usize, + max_length: usize, +} + +impl<'de, G> Deserialize<'de> for LengthBoundedDisjointPaths +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = LengthBoundedDisjointPathsData::::deserialize(deserializer)?; + Self::new(data.graph, data.source, data.sink, data.max_length) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct LengthBoundedDisjointPathsCreateSpec { /// Undirected graph edges. @@ -97,35 +117,12 @@ impl TryFrom for LengthBoundedDisjointPath .transpose()? .unwrap_or(0); let num_vertices = spec.num_vertices.unwrap_or(inferred); - if num_vertices < inferred { - return Err(format!( - "num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}" - ).into()); - } - if spec.source >= num_vertices || spec.sink >= num_vertices { - return Err("source and sink must be valid graph vertices" - .to_string() - .into()); - } - if spec.source == spec.sink { - return Err("source and sink must be distinct".to_string().into()); - } - if spec.max_length == 0 { - return Err("max_length must be positive".to_string().into()); - } - - let graph = SimpleGraph::new(num_vertices, spec.graph); - let max_paths = graph - .neighbors(spec.source) - .len() - .min(graph.neighbors(spec.sink).len()); - Ok(Self { - graph, - source: spec.source, - sink: spec.sink, - max_paths, - max_length: spec.max_length, - }) + Self::new( + SimpleGraph::new(num_vertices, spec.graph)?, + spec.source, + spec.sink, + spec.max_length, + ) } } @@ -135,31 +132,38 @@ impl LengthBoundedDisjointPaths { /// The `max_paths` upper bound is computed automatically as /// `min(deg(source), deg(sink))`. /// - /// # Panics + /// # Errors /// - /// Panics if `source` or `sink` is not a valid graph vertex, if `source == + /// Returns an error if `source` or `sink` is not a valid graph vertex, if `source == /// sink`, or if `max_length == 0`. - pub fn new(graph: G, source: usize, sink: usize, max_length: usize) -> Self { - assert!( - source < graph.num_vertices(), - "source must be a valid graph vertex" - ); - assert!( - sink < graph.num_vertices(), - "sink must be a valid graph vertex" - ); - assert_ne!(source, sink, "source and sink must be distinct"); - assert!(max_length > 0, "max_length must be positive"); + pub fn new( + graph: G, + source: usize, + sink: usize, + max_length: usize, + ) -> Result { + if !(source < graph.num_vertices()) { + return Err("source must be a valid graph vertex".into()); + } + if !(sink < graph.num_vertices()) { + return Err("sink must be a valid graph vertex".into()); + } + if source == sink { + return Err("source and sink must be distinct".into()); + } + if max_length == 0 { + return Err("max_length must be positive".into()); + } let deg_s = graph.neighbors(source).len(); let deg_t = graph.neighbors(sink).len(); let max_paths = deg_s.min(deg_t); - Self { + Ok(Self { graph, source, sink, max_paths, max_length, - } + }) } /// Get a reference to the underlying graph. @@ -243,8 +247,16 @@ impl crate::solvers::BruteForceProblem for LengthBoundedDisjointPaths where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.max_paths * self.graph.num_edges()] + fn num_variables(&self) -> Result { + (self.max_paths) + .checked_mul(self.graph.num_edges()) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -311,13 +323,13 @@ fn encode_paths(num_edges: usize, max_paths: usize, slots: &[&[usize]]) -> Vec Vec { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 4), (0, 2), (2, 4), (0, 3), (3, 4)]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 4), (0, 2), (2, 4), (0, 3), (3, 4)]).unwrap(); // max_paths = min(deg(0), deg(4)) = min(3, 3) = 3 // Three edge-selection rows over six edges. // Optimal: 3 disjoint paths [0,1,4], [0,2,4], [0,3,4] vec![crate::example_db::specs::ModelExampleSpec { id: "length_bounded_disjoint_paths_simplegraph", - instance: Box::new(LengthBoundedDisjointPaths::new(graph, 0, 4, 3)), + instance: Box::new(LengthBoundedDisjointPaths::new(graph, 0, 4, 3).unwrap()), optimal_config: serde_json::json!(encode_paths(6, 3, &[&[0, 1], &[2, 3], &[4, 5]])), optimal_value: serde_json::json!(3), }] @@ -339,12 +351,7 @@ crate::impl_random_generate!( if max_length == 0 { return Err("max_length must be positive".to_string().into()); } - Ok(LengthBoundedDisjointPaths::new( - endpoints.graph()?, - source, - sink, - max_length, - )) + Ok(LengthBoundedDisjointPaths::new(endpoints.graph()?, source, sink, max_length).unwrap()) } ); diff --git a/src/models/graph/longest_circuit.rs b/src/models/graph/longest_circuit.rs index f60e24bff..53269b59d 100644 --- a/src/models/graph/longest_circuit.rs +++ b/src/models/graph/longest_circuit.rs @@ -40,12 +40,30 @@ inventory::submit! { /// /// A valid configuration must select edges that form exactly one connected /// simple circuit using only edges from `graph`. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct LongestCircuit { graph: G, edge_lengths: Vec, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))] +struct LongestCircuitData { + graph: G, + edge_lengths: Vec, +} + +impl<'de, G, W> Deserialize<'de> for LongestCircuit +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = LongestCircuitData::::deserialize(deserializer)?; + Self::new(data.graph, data.edge_lengths).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct LongestCircuitCreateSpec { #[create(codec = "edge-list")] @@ -63,18 +81,7 @@ impl TryFrom for LongestCircuit { let edge_lengths = spec .edge_weights .unwrap_or_else(|| vec![1; graph.num_edges()]); - if edge_lengths.len() != graph.num_edges() { - return Err(format!( - "edge_weights has length {}, expected {}", - edge_lengths.len(), - graph.num_edges() - ) - .into()); - } - if edge_lengths.iter().any(|&length| length <= 0) { - return Err("edge_weights must be positive".to_string().into()); - } - Ok(Self::new(graph, edge_lengths)) + Self::new(graph, edge_lengths) } } @@ -106,33 +113,22 @@ fn simple_graph_from_create( ) .into()); } - Ok(SimpleGraph::new(num_vertices, edges)) + SimpleGraph::new(num_vertices, edges) } impl LongestCircuit { /// Create a new LongestCircuit instance. /// - /// # Panics + /// # Errors /// - /// Panics if the number of edge lengths does not match the graph's edge + /// Returns an error if the number of edge lengths does not match the graph's edge /// count, or if any edge length is non-positive. - pub fn new(graph: G, edge_lengths: Vec) -> Self { - assert_eq!( - edge_lengths.len(), - graph.num_edges(), - "edge_lengths length must match num_edges" - ); - let zero = W::Sum::zero(); - assert!( - edge_lengths - .iter() - .all(|length| length.to_sum() > zero.clone()), - "All edge lengths must be positive (> 0)" - ); - Self { + pub fn new(graph: G, edge_lengths: Vec) -> Result { + Self::check_weights(&graph, &edge_lengths)?; + Ok(Self { graph, edge_lengths, - } + }) } /// Get a reference to the underlying graph. @@ -146,25 +142,26 @@ impl LongestCircuit { } /// Replace the edge lengths. - pub fn set_lengths(&mut self, edge_lengths: Vec) { - assert_eq!( - edge_lengths.len(), - self.graph.num_edges(), - "edge_lengths length must match num_edges" - ); - let zero = W::Sum::zero(); - assert!( - edge_lengths - .iter() - .all(|length| length.to_sum() > zero.clone()), - "All edge lengths must be positive (> 0)" - ); + pub fn set_lengths( + &mut self, + edge_lengths: Vec, + ) -> Result<(), crate::registry::ConstructionError> { + Self::check_weights(&self.graph, &edge_lengths)?; self.edge_lengths = edge_lengths; + Ok(()) } - /// Replace the edge lengths via the generic weight-management naming. - pub fn set_weights(&mut self, weights: Vec) { - self.set_lengths(weights); + fn check_weights(graph: &G, weights: &[W]) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_edges() { + return Err("edge_lengths length must match num_edges".into()); + } + if !weights + .iter() + .all(|weight| weight.to_sum() > W::Sum::zero()) + { + return Err("edge_lengths must be positive (> 0)".into()); + } + Ok(()) } /// Get the edge lengths as a cloned vector. @@ -241,8 +238,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -316,24 +317,28 @@ pub(crate) fn is_simple_circuit(graph: &G, config: &[bool]) -> bool { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "longest_circuit_simplegraph", - instance: Box::new(LongestCircuit::new( - SimpleGraph::new( - 6, - vec![ - (0, 1), - (1, 2), - (2, 3), - (3, 4), - (4, 5), - (5, 0), - (0, 3), - (1, 4), - (2, 5), - (3, 5), - ], - ), - vec![3, 2, 4, 1, 5, 2, 3, 2, 1, 2], - )), + instance: Box::new( + LongestCircuit::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), + (5, 0), + (0, 3), + (1, 4), + (2, 5), + (3, 5), + ], + ) + .unwrap(), + vec![3, 2, 4, 1, 5, 2, 3, 2, 1, 2], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![ true, false, true, false, true, false, true, true, true, false ]), @@ -344,7 +349,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec, crate::random::SimpleGraphRandomSpec, |spec| { let graph = spec.graph()?; let lengths = vec![1; graph.num_edges()]; - Ok(LongestCircuit::new(graph, lengths)) + Ok(LongestCircuit::new(graph, lengths).unwrap()) }); crate::declare_variants! { diff --git a/src/models/graph/longest_path.rs b/src/models/graph/longest_path.rs index 2423f24fb..87bcb95f4 100644 --- a/src/models/graph/longest_path.rs +++ b/src/models/graph/longest_path.rs @@ -40,7 +40,7 @@ inventory::submit! { /// /// A valid configuration must select exactly the edges of one simple /// undirected path from `source_vertex` to `target_vertex`. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct LongestPath { graph: G, edge_lengths: Vec, @@ -48,6 +48,32 @@ pub struct LongestPath { target_vertex: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))] +struct LongestPathData { + graph: G, + edge_lengths: Vec, + source_vertex: usize, + target_vertex: usize, +} + +impl<'de, G, W> Deserialize<'de> for LongestPath +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = LongestPathData::::deserialize(deserializer)?; + Self::new( + data.graph, + data.edge_lengths, + data.source_vertex, + data.target_vertex, + ) + .map_err(serde::de::Error::custom) + } +} + macro_rules! longest_path_create_spec { (@lengths $spec:ident, $lengths:ident) => { $spec.$lengths }; (@lengths $spec:ident) => { vec![One; $spec.graph.len()] }; @@ -82,25 +108,8 @@ macro_rules! longest_path_create_spec { .transpose()? .unwrap_or(0); let count = spec.num_vertices.unwrap_or(inferred); - if count < inferred { - return Err("num_vertices is too small".into()); - } - let edge_lengths = longest_path_create_spec!(@lengths spec $(, $lengths)?); - if edge_lengths.len() != spec.graph.len() { - return Err("edge_lengths length must match graph edge count".into()); - } - if edge_lengths.iter().any(|v| v.to_sum() <= 0) { - return Err("edge lengths must be positive".into()); - } - if spec.source_vertex >= count || spec.target_vertex >= count { - return Err("source_vertex and target_vertex must be valid vertices".into()); - } - Ok(Self { - graph: SimpleGraph::new(count, spec.graph), - edge_lengths, - source_vertex: spec.source_vertex, - target_vertex: spec.target_vertex, - }) + let edge_lengths = longest_path_create_spec!(@lengths spec $(, $lengths)?); + Self::new(SimpleGraph::new(count, spec.graph)?, edge_lengths, spec.source_vertex, spec.target_vertex) } } }; @@ -109,42 +118,36 @@ longest_path_create_spec!(LongestPathI64CreateSpec, i64, edge_lengths); longest_path_create_spec!(LongestPathOneCreateSpec, One); impl LongestPath { - fn assert_positive_edge_lengths(edge_lengths: &[W]) { - let zero = W::Sum::zero(); - assert!( - edge_lengths - .iter() - .all(|length| length.to_sum() > zero.clone()), - "All edge lengths must be positive (> 0)" - ); - } - /// Create a new LongestPath instance. - pub fn new(graph: G, edge_lengths: Vec, source_vertex: usize, target_vertex: usize) -> Self { - assert_eq!( - edge_lengths.len(), - graph.num_edges(), - "edge_lengths length must match num_edges" - ); - Self::assert_positive_edge_lengths(&edge_lengths); - assert!( - source_vertex < graph.num_vertices(), - "source_vertex {} out of bounds (graph has {} vertices)", - source_vertex, - graph.num_vertices() - ); - assert!( - target_vertex < graph.num_vertices(), - "target_vertex {} out of bounds (graph has {} vertices)", - target_vertex, - graph.num_vertices() - ); - Self { + pub fn new( + graph: G, + edge_lengths: Vec, + source_vertex: usize, + target_vertex: usize, + ) -> Result { + Self::check_weights(&graph, &edge_lengths)?; + if !(source_vertex < graph.num_vertices()) { + return Err(format!( + "source_vertex {} out of bounds (graph has {} vertices)", + source_vertex, + graph.num_vertices() + ) + .into()); + } + if !(target_vertex < graph.num_vertices()) { + return Err(format!( + "target_vertex {} out of bounds (graph has {} vertices)", + target_vertex, + graph.num_vertices() + ) + .into()); + } + Ok(Self { graph, edge_lengths, source_vertex, target_vertex, - } + }) } /// Get a reference to the underlying graph. @@ -158,14 +161,26 @@ impl LongestPath { } /// Replace the edge lengths with a new vector. - pub fn set_lengths(&mut self, edge_lengths: Vec) { - assert_eq!( - edge_lengths.len(), - self.graph.num_edges(), - "edge_lengths length must match num_edges" - ); - Self::assert_positive_edge_lengths(&edge_lengths); + pub fn set_lengths( + &mut self, + edge_lengths: Vec, + ) -> Result<(), crate::registry::ConstructionError> { + Self::check_weights(&self.graph, &edge_lengths)?; self.edge_lengths = edge_lengths; + Ok(()) + } + + fn check_weights(graph: &G, weights: &[W]) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_edges() { + return Err("edge_lengths length must match num_edges".into()); + } + if !weights + .iter() + .all(|weight| weight.to_sum() > W::Sum::zero()) + { + return Err("edge_lengths must be positive (> 0)".into()); + } + Ok(()) } /// Get the source vertex. @@ -254,8 +269,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -273,26 +292,30 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "longest_path_simplegraph", - instance: Box::new(LongestPath::new( - SimpleGraph::new( - 7, - vec![ - (0, 1), - (0, 2), - (1, 3), - (2, 3), - (2, 4), - (3, 5), - (4, 5), - (4, 6), - (5, 6), - (1, 6), - ], - ), - vec![3, 2, 4, 1, 5, 2, 3, 2, 4, 1], - 0, - 6, - )), + instance: Box::new( + LongestPath::new( + SimpleGraph::new( + 7, + vec![ + (0, 1), + (0, 2), + (1, 3), + (2, 3), + (2, 4), + (3, 5), + (4, 5), + (4, 6), + (5, 6), + (1, 6), + ], + ) + .unwrap(), + vec![3, 2, 4, 1, 5, 2, 3, 2, 4, 1], + 0, + 6, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![ true, false, true, true, true, false, true, false, true, false ]), diff --git a/src/models/graph/max_cut.rs b/src/models/graph/max_cut.rs index 0d414cfac..9123b048b 100644 --- a/src/models/graph/max_cut.rs +++ b/src/models/graph/max_cut.rs @@ -54,8 +54,8 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Create a triangle with unit weights -/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); -/// let problem = MaxCut::new(graph, vec![1, 1, 1]); +/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); +/// let problem = MaxCut::new(graph, vec![1, 1, 1]).unwrap(); /// /// // Solve with brute force /// let solver = BruteForce::new(); @@ -67,7 +67,7 @@ inventory::submit! { /// assert_eq!(size, Max(Some(2))); /// } /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MaxCut { /// The underlying graph structure. graph: G, @@ -75,6 +75,23 @@ pub struct MaxCut { edge_weights: Vec, } +#[derive(Deserialize)] +struct MaxCutData { + graph: G, + edge_weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MaxCut +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MaxCutData::deserialize(deserializer)?; + Self::new(data.graph, data.edge_weights).map_err(serde::de::Error::custom) + } +} + macro_rules! max_cut_create_spec { ($name:ident, $weight:ty, $one:expr $(, $edge_weights:ident)?) => { #[derive(Debug, Deserialize, crate::CreateSpec)] @@ -94,15 +111,7 @@ macro_rules! max_cut_create_spec { fn try_from(spec: $name) -> Result { let graph = simple_graph_from_create(spec.graph, spec.num_vertices)?; let edge_weights = { $(if let Some(value) = spec.$edge_weights { value } else)? { vec![$one; graph.num_edges()] } }; - if edge_weights.len() != graph.num_edges() { - return Err(format!( - "edge_weights has length {}, expected {}", - edge_weights.len(), - graph.num_edges() - ) - .into()); - } - Ok(Self::new(graph, edge_weights)) + Self::new(graph, edge_weights) } } }; @@ -136,7 +145,7 @@ fn simple_graph_from_create( if num_vertices < inferred { return Err(format!("num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}").into()); } - Ok(SimpleGraph::new(num_vertices, edges)) + SimpleGraph::new(num_vertices, edges) } impl MaxCut { @@ -145,16 +154,14 @@ impl MaxCut { /// # Arguments /// * `graph` - The underlying graph /// * `edge_weights` - Weights for each edge (must match graph.num_edges()) - pub fn new(graph: G, edge_weights: Vec) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - Self { + pub fn new(graph: G, edge_weights: Vec) -> Result { + if edge_weights.len() != graph.num_edges() { + return Err("edge_weights length must match graph num_edges".into()); + } + Ok(Self { graph, edge_weights, - } + }) } /// Create a MaxCut problem with unit weights. @@ -262,8 +269,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -294,7 +305,7 @@ where crate::impl_random_generate!(MaxCut, crate::random::SimpleGraphRandomSpec, |spec| { let graph = spec.graph()?; let weights = vec![1; graph.num_edges()]; - Ok(MaxCut::new(graph, weights)) + MaxCut::new(graph, weights) }); crate::declare_variants! { @@ -312,22 +323,25 @@ pub(crate) fn canonical_model_example_specs() -> Vec::unweighted(SimpleGraph::new( - 5, - vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)], - ))), + instance: Box::new(MaxCut::<_, i64>::unweighted( + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(), + )), optimal_config: serde_json::json!(vec![true, false, false, true, false]), optimal_value: serde_json::json!(5), }, crate::example_db::specs::ModelExampleSpec { id: "max_cut_seven_edge_graph", - instance: Box::new(MaxCut::new( - SimpleGraph::new( - 5, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 4), (2, 3), (3, 4)], - ), - vec![One; 7], - )), + instance: Box::new( + MaxCut::new( + SimpleGraph::new( + 5, + vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 4), (2, 3), (3, 4)], + ) + .unwrap(), + vec![One; 7], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![false, true, false, true, false]), optimal_value: serde_json::json!(6), }, diff --git a/src/models/graph/maximal_is.rs b/src/models/graph/maximal_is.rs index 079e0752d..073227e01 100644 --- a/src/models/graph/maximal_is.rs +++ b/src/models/graph/maximal_is.rs @@ -42,8 +42,8 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Path graph 0-1-2 -/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); -/// let problem = MaximalIS::new(graph, vec![1; 3]); +/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); +/// let problem = MaximalIS::new(graph, vec![1; 3]).unwrap(); /// /// let solver = BruteForce::new(); /// let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -53,7 +53,7 @@ inventory::submit! { /// assert!(problem.evaluate(sol).unwrap().is_valid()); /// } /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MaximalIS { /// The underlying graph. graph: G, @@ -61,6 +61,23 @@ pub struct MaximalIS { weights: Vec, } +#[derive(Deserialize)] +struct MaximalISData { + graph: G, + weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MaximalIS +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MaximalISData::deserialize(deserializer)?; + Self::new(data.graph, data.weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MaximalISCreateSpec { /// The underlying graph G=(V,E). @@ -72,27 +89,17 @@ struct MaximalISCreateSpec { impl TryFrom for MaximalIS { type Error = crate::registry::ConstructionError; fn try_from(spec: MaximalISCreateSpec) -> Result { - if spec.weights.len() != spec.graph.num_vertices() { - return Err(format!( - "weights has {} entries, expected {}", - spec.weights.len(), - spec.graph.num_vertices() - ) - .into()); - } - Ok(Self::new(spec.graph, spec.weights)) + Self::new(spec.graph, spec.weights) } } impl MaximalIS { /// Create a Maximal Independent Set problem from a graph with given weights. - pub fn new(graph: G, weights: Vec) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - Self { graph, weights } + pub fn new(graph: G, weights: Vec) -> Result { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + Ok(Self { graph, weights }) } /// Get a reference to the underlying graph. @@ -215,8 +222,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -224,10 +235,13 @@ where pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "maximal_is_simplegraph", - instance: Box::new(MaximalIS::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), - vec![1i64; 5], - )), + instance: Box::new( + MaximalIS::new( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(), + vec![1i64; 5], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![true, false, true, false, true]), optimal_value: serde_json::json!(3), }] @@ -266,7 +280,7 @@ pub(crate) fn is_maximal_independent_set(graph: &G, selected: &[bool]) } crate::impl_random_generate!(MaximalIS, crate::random::SimpleGraphRandomSpec, |spec| { - Ok(MaximalIS::new(spec.graph()?, vec![1; spec.num_vertices])) + MaximalIS::new(spec.graph()?, vec![1; spec.num_vertices]) }); crate::declare_variants! { diff --git a/src/models/graph/maximum_achromatic_number.rs b/src/models/graph/maximum_achromatic_number.rs index 6046f97e1..99fc12058 100644 --- a/src/models/graph/maximum_achromatic_number.rs +++ b/src/models/graph/maximum_achromatic_number.rs @@ -50,7 +50,7 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // C6: achromatic number is 3 -/// let graph = SimpleGraph::new(6, vec![(0,1),(1,2),(2,3),(3,4),(4,5),(5,0)]); +/// let graph = SimpleGraph::new(6, vec![(0,1),(1,2),(2,3),(3,4),(4,5),(5,0)]).unwrap(); /// let problem = MaximumAchromaticNumber::new(graph); /// /// let solver = BruteForce::new(); @@ -172,8 +172,12 @@ impl crate::solvers::BruteForceProblem for MaximumAchromaticNumber where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.graph.num_vertices(); self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } @@ -197,10 +201,9 @@ pub(crate) fn canonical_model_example_specs() -> Vec { /// The underlying graph. graph: G, @@ -64,6 +64,23 @@ pub struct MaximumClique { weights: Vec, } +#[derive(Deserialize)] +struct MaximumCliqueData { + graph: G, + weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MaximumClique +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MaximumCliqueData::deserialize(deserializer)?; + Self::new(data.graph, data.weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MaximumCliqueCreateSpec { /// The underlying graph G=(V,E). @@ -75,27 +92,17 @@ struct MaximumCliqueCreateSpec { impl TryFrom> for MaximumClique { type Error = crate::registry::ConstructionError; fn try_from(spec: MaximumCliqueCreateSpec) -> Result { - if spec.weights.len() != spec.graph.num_vertices() { - return Err(format!( - "weights has {} entries, expected {}", - spec.weights.len(), - spec.graph.num_vertices() - ) - .into()); - } - Ok(Self::new(spec.graph, spec.weights)) + Self::new(spec.graph, spec.weights) } } impl MaximumClique { /// Create a MaximumClique problem from a graph with given weights. - pub fn new(graph: G, weights: Vec) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - Self { graph, weights } + pub fn new(graph: G, weights: Vec) -> Result { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + Ok(Self { graph, weights }) } /// Get a reference to the underlying graph. @@ -182,8 +189,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -209,10 +220,10 @@ fn is_clique_config(graph: &G, config: &[bool]) -> bool { } crate::impl_random_generate!(MaximumClique, crate::random::SimpleGraphRandomSpec, |spec| { - Ok(MaximumClique::new(spec.graph()?, vec![1; spec.num_vertices])) + MaximumClique::new(spec.graph()?, vec![1; spec.num_vertices]) }); crate::impl_random_generate!(MaximumClique, crate::random::SimpleGraphRandomSpec, |spec| { - Ok(MaximumClique::new(spec.graph()?, vec![One; spec.num_vertices])) + MaximumClique::new(spec.graph()?, vec![One; spec.num_vertices]) }); #[derive(Debug, Deserialize, crate::CreateSpec)] @@ -225,7 +236,7 @@ impl TryFrom for MaximumClique { type Error = crate::registry::ConstructionError; fn try_from(spec: MaximumCliqueOneCreateSpec) -> Result { let weights = vec![One; spec.graph.num_vertices()]; - Ok(Self::new(spec.graph, weights)) + Self::new(spec.graph, weights) } } @@ -243,10 +254,13 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "maximum_clique_simplegraph", - instance: Box::new(MaximumClique::new( - SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]), - vec![1i64; 5], - )), + instance: Box::new( + MaximumClique::new( + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(), + vec![1i64; 5], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![false, false, true, true, true]), optimal_value: serde_json::json!(3), }] diff --git a/src/models/graph/maximum_co_k_plex.rs b/src/models/graph/maximum_co_k_plex.rs index 29495f0d6..eb235ab44 100644 --- a/src/models/graph/maximum_co_k_plex.rs +++ b/src/models/graph/maximum_co_k_plex.rs @@ -58,13 +58,12 @@ inventory::submit! { /// use problemreductions::{BruteForce, Problem}; /// /// // 5-cycle C_5 with k = 2 (induced degree <= 1). -/// let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]); +/// let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(); /// let problem = -/// MaximumCoKPlex::<_, One, KN>::with_k(graph, vec![One; 5], 2); +/// MaximumCoKPlex::<_, One, KN>::with_k(graph, vec![One; 5], 2).unwrap(); /// assert_eq!(problem.bound_k(), 2); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(bound(deserialize = "G: serde::Deserialize<'de>, W: serde::Deserialize<'de>"))] +#[derive(Debug, Clone, Serialize)] pub struct MaximumCoKPlex { /// The underlying graph. graph: G, @@ -81,6 +80,25 @@ pub struct MaximumCoKPlex { _phantom: std::marker::PhantomData, } +#[derive(Deserialize)] +struct MaximumCoKPlexData { + graph: G, + weights: Vec, + bound_k: usize, +} + +impl<'de, G, W, K> Deserialize<'de> for MaximumCoKPlex +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, + K: KValue, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MaximumCoKPlexData::deserialize(deserializer)?; + Self::with_k(data.graph, data.weights, data.bound_k).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MaximumCoKPlexCreateSpec { /// The underlying graph G=(V,E). @@ -97,56 +115,48 @@ impl TryFrom> type Error = crate::registry::ConstructionError; fn try_from(spec: MaximumCoKPlexCreateSpec) -> Result { - if spec.weights.len() != spec.graph.num_vertices() { - return Err(format!( - "weights has {} entries, expected {}", - spec.weights.len(), - spec.graph.num_vertices() - ) - .into()); - } - if spec.k == 0 { - return Err("k must be at least 1".to_string().into()); - } - Ok(Self::with_k(spec.graph, spec.weights, spec.k)) + Self::with_k(spec.graph, spec.weights, spec.k) } } impl MaximumCoKPlex { /// Create an instance with an explicit runtime `k`. /// - /// # Panics - /// Panics if `weights.len()` does not match `graph.num_vertices()`, if + /// # Errors + /// Returns an error if `weights.len()` does not match `graph.num_vertices()`, if /// `bound_k == 0`, or if `K` declares a fixed value that disagrees with /// `bound_k`. - pub fn with_k(graph: G, weights: Vec, bound_k: usize) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - assert!(bound_k >= 1, "co-k-plex parameter k must be at least 1"); + pub fn with_k( + graph: G, + weights: Vec, + bound_k: usize, + ) -> Result { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + if bound_k == 0 { + return Err("co-k-plex parameter k must be at least 1".into()); + } if let Some(fixed) = K::K { - assert_eq!( - fixed, bound_k, - "fixed K type disagrees with runtime bound_k" - ); + if fixed != bound_k { + return Err("fixed K type disagrees with runtime bound_k".into()); + } } - Self { + Ok(Self { graph, weights, bound_k, _phantom: std::marker::PhantomData, - } + }) } /// Create a new instance using the compile-time `K`. /// - /// # Panics - /// Panics if `K` is [`KN`] (use [`MaximumCoKPlex::with_k`] instead) or if + /// # Errors + /// Returns an error if `K` is [`KN`] (use [`MaximumCoKPlex::with_k`] instead) or if /// `weights.len()` does not match `graph.num_vertices()`. - pub fn new(graph: G, weights: Vec) -> Self { - let k = K::K.expect("KN requires with_k"); + pub fn new(graph: G, weights: Vec) -> Result { + let k = K::K.ok_or("KN requires with_k")?; Self::with_k(graph, weights, k) } @@ -241,8 +251,12 @@ where W: WeightElement + VariantParam, K: KValue, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -279,10 +293,7 @@ impl TryFrom for MaximumCoKPlex Result { let weights = vec![One; spec.graph.num_vertices()]; - if spec.k == 0 { - return Err("k must be at least 1".into()); - } - Ok(Self::with_k(spec.graph, weights, spec.k)) + Self::with_k(spec.graph, weights, spec.k) } } @@ -300,11 +311,14 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "maximum_co_k_plex_simplegraph", - instance: Box::new(MaximumCoKPlex::<_, i64, KN>::with_k( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]), - vec![5, 1, 4, 1, 3], - 2, - )), + instance: Box::new( + MaximumCoKPlex::<_, i64, KN>::with_k( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(), + vec![5, 1, 4, 1, 3], + 2, + ) + .unwrap(), + ), optimal_config: serde_json::json!([true, false, true, false, true]), optimal_value: serde_json::json!(12), }] diff --git a/src/models/graph/maximum_common_edge_subgraph.rs b/src/models/graph/maximum_common_edge_subgraph.rs index 179151b64..20a20e7fe 100644 --- a/src/models/graph/maximum_common_edge_subgraph.rs +++ b/src/models/graph/maximum_common_edge_subgraph.rs @@ -69,32 +69,51 @@ impl LabelledArc { /// vector and treated as a set (duplicates are deduplicated by the /// constructor). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "LabelledDigraphData")] pub struct LabelledDigraph { /// Number of vertices `|V|`. - pub num_vertices: usize, + num_vertices: usize, /// Labelled directed arcs `(u, label, v)`. - pub arcs: Vec, + arcs: Vec, +} + +#[derive(Deserialize)] +struct LabelledDigraphData { + num_vertices: usize, + arcs: Vec, +} + +impl TryFrom for LabelledDigraph { + type Error = crate::registry::ConstructionError; + fn try_from(data: LabelledDigraphData) -> Result { + Self::new(data.num_vertices, data.arcs) + } } impl LabelledDigraph { /// Construct a new labelled digraph. /// - /// # Panics - /// Panics if any arc references a vertex index outside `0..num_vertices`. - pub fn new(num_vertices: usize, arcs: Vec) -> Self { + /// # Errors + /// Returns an error if any arc references a vertex index outside `0..num_vertices`. + pub fn new( + num_vertices: usize, + arcs: Vec, + ) -> Result { for arc in &arcs { - assert!( - arc.src < num_vertices, - "labelled arc source {} out of range for num_vertices = {}", - arc.src, - num_vertices - ); - assert!( - arc.dst < num_vertices, - "labelled arc destination {} out of range for num_vertices = {}", - arc.dst, - num_vertices - ); + if !(arc.src < num_vertices) { + return Err(format!( + "labelled arc source {} out of range for num_vertices = {}", + arc.src, num_vertices + ) + .into()); + }; + if !(arc.dst < num_vertices) { + return Err(format!( + "labelled arc destination {} out of range for num_vertices = {}", + arc.dst, num_vertices + ) + .into()); + }; } // Deduplicate while preserving order so set semantics hold. let mut seen = std::collections::HashSet::new(); @@ -104,10 +123,10 @@ impl LabelledDigraph { deduped.push(arc); } } - Self { + Ok(Self { num_vertices, arcs: deduped, - } + }) } /// Number of vertices `|V|`. @@ -136,7 +155,7 @@ impl LabelledDigraph { /// /// # Configuration encoding /// -/// `dims()` returns `vec![graph_2.num_vertices + 1; graph_1.num_vertices]`. +/// The coordinate cardinalities are `vec![graph_2.num_vertices + 1; graph_1.num_vertices]`. /// For each source vertex `u in V1`, `config[u]` is either an index in /// `0..graph_2.num_vertices` (the matched target vertex) or the sentinel /// value `graph_2.num_vertices` denoting `bottom` (unmatched). Feasibility @@ -293,8 +312,18 @@ impl Problem for MaximumCommonEdgeSubgraph { } impl crate::solvers::BruteForceProblem for MaximumCommonEdgeSubgraph { - fn dimensions(&self) -> Vec { - vec![self.graph_2.num_vertices() + 1; self.graph_1.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph_1.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.graph_2.num_vertices()) + .checked_add(1usize) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a coordinate cardinality".into(), + ) + }) } } @@ -321,7 +350,8 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec0, 1->1, 2->2, 3->3, // 4->bottom preserves the first five source arcs. diff --git a/src/models/graph/maximum_contact_map_overlap.rs b/src/models/graph/maximum_contact_map_overlap.rs index c7f340220..5eea5ba5d 100644 --- a/src/models/graph/maximum_contact_map_overlap.rs +++ b/src/models/graph/maximum_contact_map_overlap.rs @@ -64,7 +64,7 @@ inventory::submit! { /// /// # Configuration encoding /// -/// `dims()` returns `vec![|V_2| + 1; |V_1|]`. For each source vertex `i`, +/// The coordinate cardinalities are `vec![|V_2| + 1; |V_1|]`. For each source vertex `i`, /// `config[i] = 0` denotes `bot` (unmatched) and `config[i] = j + 1` denotes /// "matched to vertex `j in V_2`". Feasibility requires that the nonzero /// entries are pairwise distinct (injectivity) and strictly increasing along @@ -261,8 +261,14 @@ impl Problem for MaximumContactMapOverlap { } impl crate::solvers::BruteForceProblem for MaximumContactMapOverlap { - fn dimensions(&self) -> Vec { - vec![self.num_vertices_2 + 1; self.num_vertices_1] + fn num_variables(&self) -> Result { + Ok(self.num_vertices_1) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.num_vertices_2).checked_add(1usize).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } diff --git a/src/models/graph/maximum_domatic_number.rs b/src/models/graph/maximum_domatic_number.rs index fce59b0e1..35e20f4ee 100644 --- a/src/models/graph/maximum_domatic_number.rs +++ b/src/models/graph/maximum_domatic_number.rs @@ -44,7 +44,7 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Path graph P3: 0-1-2 -/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); +/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); /// let problem = MaximumDomaticNumber::new(graph); /// /// let solver = BruteForce::new(); @@ -177,9 +177,12 @@ impl crate::solvers::BruteForceProblem for MaximumDomaticNumber where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } @@ -201,19 +204,22 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "maximum_domatic_number_simplegraph", - instance: Box::new(MaximumDomaticNumber::new(SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (0, 3), - (1, 4), - (2, 5), - (3, 4), - (3, 5), - (4, 5), - ], - ))), + instance: Box::new(MaximumDomaticNumber::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (0, 3), + (1, 4), + (2, 5), + (3, 4), + (3, 5), + (4, 5), + ], + ) + .unwrap(), + )), optimal_config: serde_json::json!(vec![0, 1, 2, 0, 2, 1]), optimal_value: serde_json::json!(3), }] diff --git a/src/models/graph/maximum_edge_weighted_k_clique.rs b/src/models/graph/maximum_edge_weighted_k_clique.rs index 9ba384908..fedbb17aa 100644 --- a/src/models/graph/maximum_edge_weighted_k_clique.rs +++ b/src/models/graph/maximum_edge_weighted_k_clique.rs @@ -52,7 +52,7 @@ inventory::submit! { /// use problemreductions::{BruteForce, Problem}; /// /// // Graph from issue #1020: 4 vertices, triangles {0,1,2} and {0,1,3}. -/// let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]); +/// let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]).unwrap(); /// let weights = vec![5_i64, 4, -1, 1, 0]; /// let problem = MaximumEdgeWeightedKClique::new(graph, weights, 3).unwrap(); /// let solution = BruteForce::new().solve(&problem).unwrap().unwrap(); @@ -220,8 +220,12 @@ impl crate::solvers::BruteForceProblem for MaximumEdgeWeightedKClique where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -266,7 +270,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]).unwrap(), vec![5, 4, -1, 1, 0], 3, ) diff --git a/src/models/graph/maximum_independent_set.rs b/src/models/graph/maximum_independent_set.rs index 55723eb26..3be64cb95 100644 --- a/src/models/graph/maximum_independent_set.rs +++ b/src/models/graph/maximum_independent_set.rs @@ -48,8 +48,8 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Create a triangle graph (3 vertices, 3 edges) -/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); -/// let problem = MaximumIndependentSet::new(graph, vec![1; 3]); +/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); +/// let problem = MaximumIndependentSet::new(graph, vec![1; 3]).unwrap(); /// /// // Solve with brute force /// let solver = BruteForce::new(); @@ -58,7 +58,7 @@ inventory::submit! { /// // Maximum independent set in a triangle has size 1 /// assert!(solutions.iter().all(|s| s.iter().filter(|&&selected| selected).count() == 1)); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MaximumIndependentSet { /// The underlying graph. graph: G, @@ -66,6 +66,23 @@ pub struct MaximumIndependentSet { weights: Vec, } +#[derive(Deserialize)] +struct MaximumIndependentSetData { + graph: G, + weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MaximumIndependentSet +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MaximumIndependentSetData::deserialize(deserializer)?; + Self::new(data.graph, data.weights).map_err(serde::de::Error::custom) + } +} + macro_rules! simple_mis_spec { ($name:ident,$weight:ty,$one:expr $(, $weights:ident)?) => { #[derive(Debug, Deserialize, crate::CreateSpec)] @@ -98,17 +115,8 @@ macro_rules! simple_mis_spec { .transpose()? .unwrap_or(0); let count = spec.num_vertices.unwrap_or(inferred); - if count < inferred { - return Err("num_vertices is too small".into()); - } - let weights = { $(if let Some(value) = spec.$weights { value } else)? { vec![$one; count] } }; - if weights.len() != count { - return Err("weights length must match num_vertices".into()); - } - Ok(Self { - graph: SimpleGraph::new(count, spec.graph), - weights, - }) + let weights = { $(if let Some(value) = spec.$weights { value } else)? { vec![$one; count] } }; + Self::new(SimpleGraph::new(count, spec.graph)?, weights) } } }; @@ -141,13 +149,7 @@ macro_rules! grid_mis_spec { type Error = crate::registry::ConstructionError; fn try_from(spec: $name) -> Result { let weights = { $(if let Some(value) = spec.$weights { value } else)? { vec![$one; spec.positions.len()] } }; - if weights.len() != spec.positions.len() { - return Err("weights length must match positions length".into()); - } - Ok(Self { - graph: <$graph>::new(spec.positions), - weights, - }) + Self::new(<$graph>::new(spec.positions), weights) } } }; @@ -189,15 +191,7 @@ macro_rules! unit_disk_mis_spec { fn try_from(spec: $name) -> Result { let radius = spec.radius.unwrap_or(1.0); let weights = { $(if let Some(value) = spec.$weights { value } else)? { vec![$one; spec.positions.len()] } }; - if weights.len() != spec.positions.len() { - return Err(ConstructionError::Conversion( - "weights length must match positions length".into(), - )); - } - Ok(Self { - graph: UnitDiskGraph::new(spec.positions, radius)?, - weights, - }) + Self::new(UnitDiskGraph::new(spec.positions, radius)?, weights) } } }; @@ -212,13 +206,11 @@ unit_disk_mis_spec!( impl MaximumIndependentSet { /// Create an Independent Set problem from a graph with given weights. - pub fn new(graph: G, weights: Vec) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - Self { graph, weights } + pub fn new(graph: G, weights: Vec) -> Result { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + Ok(Self { graph, weights }) } /// Get a reference to the underlying graph. @@ -309,8 +301,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -325,30 +321,30 @@ fn is_independent_set_config(graph: &G, config: &[bool]) -> bool { } crate::impl_random_generate!(MaximumIndependentSet, crate::random::SimpleGraphRandomSpec, |spec| { - Ok(MaximumIndependentSet::new(spec.graph()?, vec![1; spec.num_vertices])) + MaximumIndependentSet::new(spec.graph()?, vec![1; spec.num_vertices]) }); crate::impl_random_generate!(MaximumIndependentSet, crate::random::SimpleGraphRandomSpec, |spec| { - Ok(MaximumIndependentSet::new(spec.graph()?, vec![One; spec.num_vertices])) + MaximumIndependentSet::new(spec.graph()?, vec![One; spec.num_vertices]) }); crate::impl_random_generate!(MaximumIndependentSet, crate::random::IntegerGeometryRandomSpec, |spec| { let seed = crate::random::seed_to_u64(spec.seed)?; - Ok(MaximumIndependentSet::new(KingsSubgraph::new(crate::random::create_random_int_positions(spec.num_vertices, seed)), vec![1; spec.num_vertices])) + MaximumIndependentSet::new(KingsSubgraph::new(crate::random::create_random_int_positions(spec.num_vertices, seed)), vec![1; spec.num_vertices]) }); crate::impl_random_generate!(MaximumIndependentSet, crate::random::IntegerGeometryRandomSpec, |spec| { let seed = crate::random::seed_to_u64(spec.seed)?; - Ok(MaximumIndependentSet::new(KingsSubgraph::new(crate::random::create_random_int_positions(spec.num_vertices, seed)), vec![One; spec.num_vertices])) + MaximumIndependentSet::new(KingsSubgraph::new(crate::random::create_random_int_positions(spec.num_vertices, seed)), vec![One; spec.num_vertices]) }); crate::impl_random_generate!(MaximumIndependentSet, crate::random::IntegerGeometryRandomSpec, |spec| { let seed = crate::random::seed_to_u64(spec.seed)?; - Ok(MaximumIndependentSet::new(TriangularSubgraph::new(crate::random::create_random_int_positions(spec.num_vertices, seed)), vec![1; spec.num_vertices])) + MaximumIndependentSet::new(TriangularSubgraph::new(crate::random::create_random_int_positions(spec.num_vertices, seed)), vec![1; spec.num_vertices]) }); crate::impl_random_generate!(MaximumIndependentSet, crate::random::UnitDiskRandomSpec, |spec| { let seed = crate::random::seed_to_u64(spec.seed)?; - Ok(MaximumIndependentSet::new(UnitDiskGraph::new(crate::random::create_random_float_positions(spec.num_vertices, seed), spec.radius.unwrap_or(1.0))?, vec![1; spec.num_vertices])) + MaximumIndependentSet::new(UnitDiskGraph::new(crate::random::create_random_float_positions(spec.num_vertices, seed), spec.radius.unwrap_or(1.0))?, vec![1; spec.num_vertices]) }); crate::impl_random_generate!(MaximumIndependentSet, crate::random::UnitDiskRandomSpec, |spec| { let seed = crate::random::seed_to_u64(spec.seed)?; - Ok(MaximumIndependentSet::new(UnitDiskGraph::new(crate::random::create_random_float_positions(spec.num_vertices, seed), spec.radius.unwrap_or(1.0))?, vec![One; spec.num_vertices])) + MaximumIndependentSet::new(UnitDiskGraph::new(crate::random::create_random_float_positions(spec.num_vertices, seed), spec.radius.unwrap_or(1.0))?, vec![One; spec.num_vertices]) }); crate::declare_variants! { @@ -421,29 +417,33 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec>( @@ -534,7 +538,7 @@ pub(crate) fn decision_canonical_rule_example_specs( id: "decision_maximum_independent_set_unit_to_maximum_independent_set", build: || { let source = Decision::new( - MaximumIndependentSet::new(SimpleGraph::path(3), vec![One; 3]), + MaximumIndependentSet::new(SimpleGraph::path(3), vec![One; 3]).unwrap(), 2, ); rule_example_with_witness::<_, MaximumIndependentSet>( diff --git a/src/models/graph/maximum_leaf_spanning_tree.rs b/src/models/graph/maximum_leaf_spanning_tree.rs index 9a8c8d3bd..329abbc95 100644 --- a/src/models/graph/maximum_leaf_spanning_tree.rs +++ b/src/models/graph/maximum_leaf_spanning_tree.rs @@ -43,22 +43,37 @@ inventory::submit! { /// # Type Parameters /// /// * `G` - The graph type (e.g., `SimpleGraph`) -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MaximumLeafSpanningTree { /// The underlying graph. graph: G, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct MaximumLeafSpanningTreeData { + graph: G, +} + +impl<'de, G> Deserialize<'de> for MaximumLeafSpanningTree +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MaximumLeafSpanningTreeData::::deserialize(deserializer)?; + Self::new(data.graph).map_err(serde::de::Error::custom) + } +} + impl MaximumLeafSpanningTree { /// Create a MaximumLeafSpanningTree problem from a graph. /// /// The graph must have at least 2 vertices. - pub fn new(graph: G) -> Self { - assert!( - graph.num_vertices() >= 2, - "graph must have at least 2 vertices" - ); - Self { graph } + pub fn new(graph: G) -> Result { + if !(graph.num_vertices() >= 2) { + return Err("graph must have at least 2 vertices".into()); + } + Ok(Self { graph }) } /// Get a reference to the underlying graph. @@ -183,8 +198,12 @@ impl crate::solvers::BruteForceProblem for MaximumLeafSpanningTree where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -195,7 +214,7 @@ crate::impl_random_generate!( if spec.num_vertices < 2 { return Err("num_vertices must be at least 2".to_string().into()); } - Ok(MaximumLeafSpanningTree::new(spec.graph()?)) + MaximumLeafSpanningTree::new(spec.graph()?) } ); @@ -211,20 +230,26 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "maximum_leaf_spanning_tree_simplegraph", - instance: Box::new(MaximumLeafSpanningTree::new(SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (0, 3), - (1, 4), - (2, 4), - (2, 5), - (3, 5), - (4, 5), - (1, 3), - ], - ))), + instance: Box::new( + MaximumLeafSpanningTree::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (0, 3), + (1, 4), + (2, 4), + (2, 5), + (3, 5), + (4, 5), + (1, 3), + ], + ) + .unwrap(), + ) + .unwrap(), + ), // Edges: 0:(0,1), 1:(0,2), 2:(0,3), 3:(1,4), 4:(2,4), 5:(2,5), 6:(3,5), 7:(4,5), 8:(1,3) // Tree: {(0,1),(0,2),(0,3),(2,4),(2,5)} = indices 0,1,2,4,5 // Leaves: 1,3,4,5 (degree 1 each), Internal: 0 (deg 3), 2 (deg 3) diff --git a/src/models/graph/maximum_matching.rs b/src/models/graph/maximum_matching.rs index 6e68acfd5..80c39e38c 100644 --- a/src/models/graph/maximum_matching.rs +++ b/src/models/graph/maximum_matching.rs @@ -45,7 +45,7 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Path graph 0-1-2 -/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); +/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); /// let problem = MaximumMatching::<_, i64>::unit_weights(graph); /// /// let solver = BruteForce::new(); @@ -56,7 +56,7 @@ inventory::submit! { /// assert_eq!(sol.iter().filter(|&&selected| selected).count(), 1); /// } /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MaximumMatching { /// The underlying graph. graph: G, @@ -64,6 +64,23 @@ pub struct MaximumMatching { edge_weights: Vec, } +#[derive(Deserialize)] +struct MaximumMatchingData { + graph: G, + edge_weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MaximumMatching +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MaximumMatchingData::deserialize(deserializer)?; + Self::new(data.graph, data.edge_weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MaximumMatchingCreateSpec { #[create(codec = "edge-list")] @@ -81,15 +98,7 @@ impl TryFrom for MaximumMatching { let edge_weights = spec .edge_weights .unwrap_or_else(|| vec![1; graph.num_edges()]); - if edge_weights.len() != graph.num_edges() { - return Err(format!( - "edge_weights has length {}, expected {}", - edge_weights.len(), - graph.num_edges() - ) - .into()); - } - Ok(Self::new(graph, edge_weights)) + Self::new(graph, edge_weights) } } @@ -121,7 +130,7 @@ fn simple_graph_from_create( ) .into()); } - Ok(SimpleGraph::new(num_vertices, edges)) + SimpleGraph::new(num_vertices, edges) } impl MaximumMatching { @@ -130,16 +139,12 @@ impl MaximumMatching { /// # Arguments /// * `graph` - The graph /// * `edge_weights` - Weight for each edge (in graph.edges() order) - pub fn new(graph: G, edge_weights: Vec) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - Self { + pub fn new(graph: G, edge_weights: Vec) -> Result { + Self::check_weights(&graph, &edge_weights)?; + Ok(Self { graph, edge_weights, - } + }) } /// Create a MaximumMatching problem with unit weights. @@ -208,9 +213,23 @@ impl MaximumMatching { } /// Set new weights for the problem. - pub fn set_weights(&mut self, weights: Vec) { - assert_eq!(weights.len(), self.graph.num_edges()); + pub fn set_weights( + &mut self, + weights: Vec, + ) -> Result<(), crate::registry::ConstructionError> { + Self::check_weights(&self.graph, &weights)?; self.edge_weights = weights; + Ok(()) + } + + fn check_weights( + graph: &G, + edge_weights: &[W], + ) -> Result<(), crate::registry::ConstructionError> { + if edge_weights.len() != graph.num_edges() { + return Err("edge_weights length must match graph num_edges".into()); + } + Ok(()) } /// Get the weights for the problem. @@ -289,15 +308,19 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } crate::impl_random_generate!(MaximumMatching, crate::random::SimpleGraphRandomSpec, |spec| { let graph = spec.graph()?; let weights = vec![1; graph.num_edges()]; - Ok(MaximumMatching::new(graph, weights)) + MaximumMatching::new(graph, weights) }); crate::declare_variants! { @@ -312,10 +335,9 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "maximum_matching_simplegraph", - instance: Box::new(MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new( - 5, - vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)], - ))), + instance: Box::new(MaximumMatching::<_, i64>::unit_weights( + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(), + )), optimal_config: serde_json::json!(vec![true, false, false, false, true, false]), optimal_value: serde_json::json!(2), }] diff --git a/src/models/graph/min_max_multicenter.rs b/src/models/graph/min_max_multicenter.rs index bcc697aff..55e57e48d 100644 --- a/src/models/graph/min_max_multicenter.rs +++ b/src/models/graph/min_max_multicenter.rs @@ -46,14 +46,14 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Hexagonal-like graph: 6 vertices, 7 edges, unit weights/lengths, K=2 -/// let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 5), (1, 4)]); -/// let problem = MinMaxMulticenter::new(graph, vec![1i64; 6], vec![1i64; 7], 2); +/// let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 5), (1, 4)]).unwrap(); +/// let problem = MinMaxMulticenter::new(graph, vec![1i64; 6], vec![1i64; 7], 2).unwrap(); /// /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinMaxMulticenter { /// The underlying graph. graph: G, @@ -65,6 +65,27 @@ pub struct MinMaxMulticenter { k: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))] +struct MinMaxMulticenterData { + graph: G, + vertex_weights: Vec, + edge_lengths: Vec, + k: usize, +} + +impl<'de, G, W> Deserialize<'de> for MinMaxMulticenter +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinMaxMulticenterData::::deserialize(deserializer)?; + Self::new(data.graph, data.vertex_weights, data.edge_lengths, data.k) + .map_err(serde::de::Error::custom) + } +} + macro_rules! min_max_multicenter_create_spec { ($name:ident, $weight:ty, $one:expr $(, $weights:ident, $edge_weights:ident)?) => { #[derive(Debug, Deserialize, crate::CreateSpec)] @@ -89,40 +110,8 @@ macro_rules! min_max_multicenter_create_spec { fn try_from(spec: $name) -> Result { let graph = simple_graph_from_create(spec.graph, spec.num_vertices)?; let vertex_weights = { $(if let Some(value) = spec.$weights { value } else)? { vec![$one; graph.num_vertices()] } }; - if vertex_weights.len() != graph.num_vertices() { - return Err(format!( - "weights has length {}, expected {}", - vertex_weights.len(), - graph.num_vertices() - ) - .into()); - } let edge_lengths = { $(if let Some(value) = spec.$edge_weights { value } else)? { vec![$one; graph.num_edges()] } }; - if edge_lengths.len() != graph.num_edges() { - return Err(format!( - "edge_weights has length {}, expected {}", - edge_lengths.len(), - graph.num_edges() - ) - .into()); - } - let zero = <$weight as WeightElement>::Sum::zero(); - if vertex_weights - .iter() - .any(|weight| weight.to_sum() < zero.clone()) - { - return Err("weights must be non-negative".to_string().into()); - } - if edge_lengths - .iter() - .any(|weight| weight.to_sum() < zero.clone()) - { - return Err("edge_weights must be non-negative".to_string().into()); - } - if spec.k == 0 || spec.k > graph.num_vertices() { - return Err(format!("k must be between 1 and {}", graph.num_vertices()).into()); - } - Ok(Self::new(graph, vertex_weights, edge_lengths, spec.k)) + Self::new(graph, vertex_weights, edge_lengths, spec.k) } } }; @@ -162,49 +151,54 @@ fn simple_graph_from_create( if num_vertices < inferred { return Err(format!("num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}").into()); } - Ok(SimpleGraph::new(num_vertices, edges)) + SimpleGraph::new(num_vertices, edges) } impl MinMaxMulticenter { /// Create a MinMaxMulticenter problem. /// - /// # Panics + /// # Errors /// - If `vertex_weights.len() != graph.num_vertices()` /// - If `edge_lengths.len() != graph.num_edges()` /// - If any vertex weight or edge length is negative /// - If `k == 0` or `k > graph.num_vertices()` - pub fn new(graph: G, vertex_weights: Vec, edge_lengths: Vec, k: usize) -> Self { - assert_eq!( - vertex_weights.len(), - graph.num_vertices(), - "vertex_weights length must match num_vertices" - ); - assert_eq!( - edge_lengths.len(), - graph.num_edges(), - "edge_lengths length must match num_edges" - ); + pub fn new( + graph: G, + vertex_weights: Vec, + edge_lengths: Vec, + k: usize, + ) -> Result { + if vertex_weights.len() != graph.num_vertices() { + return Err("vertex_weights length must match num_vertices".into()); + } + if edge_lengths.len() != graph.num_edges() { + return Err("edge_lengths length must match num_edges".into()); + } let zero = W::Sum::zero(); - assert!( - vertex_weights - .iter() - .all(|weight| weight.to_sum() >= zero.clone()), - "vertex_weights must be non-negative" - ); - assert!( - edge_lengths - .iter() - .all(|length| length.to_sum() >= zero.clone()), - "edge_lengths must be non-negative" - ); - assert!(k > 0, "k must be positive"); - assert!(k <= graph.num_vertices(), "k must not exceed num_vertices"); - Self { + if !(vertex_weights + .iter() + .all(|weight| weight.to_sum() >= zero.clone())) + { + return Err("vertex_weights must be non-negative".into()); + } + if !(edge_lengths + .iter() + .all(|length| length.to_sum() >= zero.clone())) + { + return Err("edge_lengths must be non-negative".into()); + } + if k == 0 { + return Err("k must be positive".into()); + } + if !(k <= graph.num_vertices()) { + return Err("k must not exceed num_vertices".into()); + } + Ok(Self { graph, vertex_weights, edge_lengths, k, - } + }) } /// Get a reference to the underlying graph. @@ -381,8 +375,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -400,15 +398,19 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "min_max_multicenter_simplegraph", - instance: Box::new(MinMaxMulticenter::new( - SimpleGraph::new( - 6, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 5), (1, 4)], - ), - vec![1i64; 6], - vec![1i64; 7], - 2, - )), + instance: Box::new( + MinMaxMulticenter::new( + SimpleGraph::new( + 6, + vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 5), (1, 4)], + ) + .unwrap(), + vec![1i64; 6], + vec![1i64; 7], + 2, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![false, true, false, false, true, false]), optimal_value: serde_json::json!(1), }] diff --git a/src/models/graph/minimum_capacitated_spanning_tree.rs b/src/models/graph/minimum_capacitated_spanning_tree.rs index 631a960b6..e167ef6d8 100644 --- a/src/models/graph/minimum_capacitated_spanning_tree.rs +++ b/src/models/graph/minimum_capacitated_spanning_tree.rs @@ -48,7 +48,7 @@ inventory::submit! { /// /// * `G` - The graph type (e.g., `SimpleGraph`) /// * `W` - The weight type for edges and requirements (e.g., `i64`) -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumCapacitatedSpanningTree { /// The underlying graph. graph: G, @@ -62,6 +62,37 @@ pub struct MinimumCapacitatedSpanningTree { capacity: W::Sum, } +#[derive(Deserialize)] +#[serde(bound( + deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>" +))] +struct MinimumCapacitatedSpanningTreeData { + graph: G, + weights: Vec, + root: usize, + requirements: Vec, + capacity: W::Sum, +} + +impl<'de, G, W> Deserialize<'de> for MinimumCapacitatedSpanningTree +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, + W::Sum: Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumCapacitatedSpanningTreeData::::deserialize(deserializer)?; + Self::new( + data.graph, + data.weights, + data.root, + data.requirements, + data.capacity, + ) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumCapacitatedSpanningTreeCreateSpec { /// The underlying graph. @@ -82,37 +113,20 @@ impl TryFrom fn try_from(spec: MinimumCapacitatedSpanningTreeCreateSpec) -> Result { let edges = spec.graph.num_edges(); let weights = spec.weights.unwrap_or_else(|| vec![1; edges]); - if weights.len() != edges { - return Err(format!("weights has {} entries, expected {edges}", weights.len()).into()); - } - let vertices = spec.graph.num_vertices(); - if vertices < 2 { - return Err("graph must have at least two vertices".to_string().into()); - } - if spec.requirements.len() != vertices { - return Err(format!( - "requirements has {} entries, expected {vertices}", - spec.requirements.len() - ) - .into()); - } - if spec.root >= vertices { - return Err("root is outside the graph".to_string().into()); - } - Ok(Self::new( + Self::new( spec.graph, weights, spec.root, spec.requirements, spec.capacity, - )) + ) } } impl MinimumCapacitatedSpanningTree { /// Create a MinimumCapacitatedSpanningTree problem. /// - /// # Panics + /// # Errors /// - If `weights.len() != graph.num_edges()` /// - If `requirements.len() != graph.num_vertices()` /// - If `root >= graph.num_vertices()` @@ -123,33 +137,28 @@ impl MinimumCapacitatedSpanningTree { root: usize, requirements: Vec, capacity: W::Sum, - ) -> Self { - assert_eq!( - weights.len(), - graph.num_edges(), - "weights length must match num_edges" - ); - assert_eq!( - requirements.len(), - graph.num_vertices(), - "requirements length must match num_vertices" - ); - assert!( - root < graph.num_vertices(), - "root {root} out of range (num_vertices = {})", - graph.num_vertices() - ); - assert!( - graph.num_vertices() >= 2, - "graph must have at least 2 vertices" - ); - Self { + ) -> Result { + Self::check_weights(&graph, &weights)?; + if requirements.len() != graph.num_vertices() { + return Err("requirements length must match num_vertices".into()); + } + if !(root < graph.num_vertices()) { + return Err(format!( + "root {root} out of range (num_vertices = {})", + graph.num_vertices() + ) + .into()); + } + if !(graph.num_vertices() >= 2) { + return Err("graph must have at least 2 vertices".into()); + } + Ok(Self { graph, weights, root, requirements, capacity, - } + }) } /// Get a reference to the underlying graph. @@ -163,9 +172,20 @@ impl MinimumCapacitatedSpanningTree { } /// Set new edge weights. - pub fn set_weights(&mut self, weights: Vec) { - assert_eq!(weights.len(), self.graph.num_edges()); + pub fn set_weights( + &mut self, + weights: Vec, + ) -> Result<(), crate::registry::ConstructionError> { + Self::check_weights(&self.graph, &weights)?; self.weights = weights; + Ok(()) + } + + fn check_weights(graph: &G, weights: &[W]) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_edges() { + return Err("weights length must match num_edges".into()); + } + Ok(()) } /// Check if the problem uses a non-unit weight type. @@ -389,8 +409,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -406,25 +430,29 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "minimum_capacitated_spanning_tree_simplegraph", - instance: Box::new(MinimumCapacitatedSpanningTree::new( - SimpleGraph::new( - 5, - vec![ - (0, 1), - (0, 2), - (0, 3), - (1, 2), - (1, 4), - (2, 3), - (2, 4), - (3, 4), - ], - ), - vec![2, 1, 4, 3, 1, 2, 3, 1], // edge weights - 0, // root - vec![0, 1, 1, 1, 1], // requirements (root=0) - 3, // capacity - )), + instance: Box::new( + MinimumCapacitatedSpanningTree::new( + SimpleGraph::new( + 5, + vec![ + (0, 1), + (0, 2), + (0, 3), + (1, 2), + (1, 4), + (2, 3), + (2, 4), + (3, 4), + ], + ) + .unwrap(), + vec![2, 1, 4, 3, 1, 2, 3, 1], // edge weights + 0, // root + vec![0, 1, 1, 1, 1], // requirements (root=0) + 3, // capacity + ) + .unwrap(), + ), // Optimal: edges {(0,1),(0,2),(1,4),(3,4)} = indices {0,1,4,7} // Weight = 2+1+1+1 = 5 // Subtree sums: subtree(1)={1,4}->req=2<=3, subtree(2)={2}->req=1<=3, diff --git a/src/models/graph/minimum_cost_circulation.rs b/src/models/graph/minimum_cost_circulation.rs index a61cea112..0d30b9d56 100644 --- a/src/models/graph/minimum_cost_circulation.rs +++ b/src/models/graph/minimum_cost_circulation.rs @@ -73,18 +73,19 @@ inventory::submit! { /// // optimal. /// let graph = DirectedGraph::new(3, vec![ /// (0, 1), (1, 0), (0, 2), (2, 0), -/// ]); +/// ]).unwrap(); /// let problem = MinimumCostCirculation::new( /// graph, /// vec![2, 2, 1, 1], // capacities /// vec![2, -3, 1, -4], // costs (signed) -/// ); +/// ).unwrap(); /// let solver = BruteForce::new(); /// let witness = solver.solve(&problem).unwrap().unwrap(); /// // Optimal cost = 2*2 + 2*(-3) + 1*1 + 1*(-4) = -5. /// assert_eq!(problem.total_cost(&witness).unwrap(), -5); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumCostCirculationData")] pub struct MinimumCostCirculation { /// The directed multigraph G = (V, A). graph: DirectedGraph, @@ -94,39 +95,57 @@ pub struct MinimumCostCirculation { costs: Vec, } +#[derive(Deserialize)] +struct MinimumCostCirculationData { + graph: DirectedGraph, + capacities: Vec, + costs: Vec, +} + +impl TryFrom for MinimumCostCirculation { + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumCostCirculationData) -> Result { + Self::new(data.graph, data.capacities, data.costs) + } +} + impl MinimumCostCirculation { /// Create a new Minimum-Cost Circulation problem. /// - /// # Panics + /// # Errors /// - /// Panics if any of the following holds: + /// Returns an error if any of the following holds: /// - `capacities.len() != graph.num_arcs()` /// - `costs.len() != graph.num_arcs()` /// - Any capacity is negative /// /// Note: costs are signed and **may be negative**. - pub fn new(graph: DirectedGraph, capacities: Vec, costs: Vec) -> Self { + pub fn new( + graph: DirectedGraph, + capacities: Vec, + costs: Vec, + ) -> Result { let m = graph.num_arcs(); - assert_eq!( - capacities.len(), - m, - "capacities length ({}) must match num_arcs ({m})", - capacities.len() - ); - assert_eq!( - costs.len(), - m, - "costs length ({}) must match num_arcs ({m})", - costs.len() - ); + if capacities.len() != m { + return Err(format!( + "capacities length ({}) must match num_arcs ({m})", + capacities.len() + ) + .into()); + } + if costs.len() != m { + return Err(format!("costs length ({}) must match num_arcs ({m})", costs.len()).into()); + } for (i, &c) in capacities.iter().enumerate() { - assert!(c >= 0, "capacity[{i}] = {c} is negative"); + if !(c >= 0) { + return Err(format!("capacity[{i}] = {c} is negative").into()); + } } - Self { + Ok(Self { graph, capacities, costs, - } + }) } /// Get a reference to the underlying directed graph. @@ -255,8 +274,12 @@ impl Problem for MinimumCostCirculation { } impl crate::solvers::BruteForceProblem for MinimumCostCirculation { - fn dimensions(&self) -> Vec { - self.capacities.iter().map(|&c| (c as usize) + 1).collect() + fn num_variables(&self) -> Result { + Ok(self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.capacities[variable]) + 1)?) } } @@ -277,10 +300,11 @@ pub(crate) fn canonical_model_example_specs() -> Vec1) = 2, arc 1 (1->0) = 2, arc 2 (0->2) = 1, arc 3 (2->0) = 1 // cost = 2*2 + 2*(-3) + 1*1 + 1*(-4) = 4 - 6 + 1 - 4 = -5 let problem = MinimumCostCirculation::new( - crate::topology::DirectedGraph::new(3, vec![(0, 1), (1, 0), (0, 2), (2, 0)]), + crate::topology::DirectedGraph::new(3, vec![(0, 1), (1, 0), (0, 2), (2, 0)]).unwrap(), vec![2, 2, 1, 1], vec![2, -3, 1, -4], - ); + ) + .unwrap(); let optimal_config = vec![2, 2, 1, 1]; let optimal_value = problem .evaluate(&optimal_config) diff --git a/src/models/graph/minimum_cost_maximum_flow.rs b/src/models/graph/minimum_cost_maximum_flow.rs index 78cf0e271..04a79b3cc 100644 --- a/src/models/graph/minimum_cost_maximum_flow.rs +++ b/src/models/graph/minimum_cost_maximum_flow.rs @@ -80,7 +80,7 @@ inventory::submit! { /// // Diamond network from the canonical example. /// let graph = DirectedGraph::new(4, vec![ /// (0, 1), (0, 2), (1, 2), (1, 3), (2, 3), -/// ]); +/// ]).unwrap(); /// let problem = MinimumCostMaximumFlow::new( /// graph, /// 0, 3, @@ -436,8 +436,12 @@ impl Problem for MinimumCostMaximumFlow { } impl crate::solvers::BruteForceProblem for MinimumCostMaximumFlow { - fn dimensions(&self) -> Vec { - self.capacities.iter().map(|&c| (c as usize) + 1).collect() + fn num_variables(&self) -> Result { + Ok(self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.capacities[variable]) + 1)?) } } @@ -452,7 +456,8 @@ crate::register_brute_force! { #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { let problem = MinimumCostMaximumFlow::new( - crate::topology::DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]), + crate::topology::DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]) + .unwrap(), 0, 3, vec![2, 1, 1, 1, 2], diff --git a/src/models/graph/minimum_covering_by_cliques.rs b/src/models/graph/minimum_covering_by_cliques.rs index 1f59f2bae..05e2e693c 100644 --- a/src/models/graph/minimum_covering_by_cliques.rs +++ b/src/models/graph/minimum_covering_by_cliques.rs @@ -48,7 +48,7 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Triangle: 3 edges can be covered by 1 clique -/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); +/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); /// let problem = MinimumCoveringByCliques::new(graph); /// /// let solver = BruteForce::new(); @@ -170,8 +170,12 @@ impl crate::solvers::BruteForceProblem for MinimumCoveringByCliques where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.graph.num_edges(); self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_edges()) } } @@ -201,20 +205,23 @@ pub(crate) fn canonical_model_example_specs() -> Vec { /// The underlying graph structure. graph: G, @@ -70,6 +70,34 @@ pub struct MinimumCutIntoBoundedSets { size_bound: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))] +struct MinimumCutIntoBoundedSetsData { + graph: G, + edge_weights: Vec, + source: usize, + sink: usize, + size_bound: usize, +} + +impl<'de, G, W> Deserialize<'de> for MinimumCutIntoBoundedSets +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumCutIntoBoundedSetsData::::deserialize(deserializer)?; + Self::new( + data.graph, + data.edge_weights, + data.source, + data.sink, + data.size_bound, + ) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumCutIntoBoundedSetsCreateSpec { /// The undirected graph. @@ -88,26 +116,13 @@ impl TryFrom for MinimumCutIntoBoundedSets< fn try_from(spec: MinimumCutIntoBoundedSetsCreateSpec) -> Result { let count = spec.graph.num_edges(); let edge_weights = spec.edge_weights.unwrap_or_else(|| vec![1; count]); - if edge_weights.len() != count { - return Err(format!( - "edge_weights has {} entries, expected {count}", - edge_weights.len() - ) - .into()); - } - let vertices = spec.graph.num_vertices(); - if spec.source >= vertices || spec.sink >= vertices || spec.source == spec.sink { - return Err("source and sink must be distinct valid graph vertices" - .to_string() - .into()); - } - Ok(Self::new( + Self::new( spec.graph, edge_weights, spec.source, spec.sink, spec.size_bound, - )) + ) } } @@ -121,8 +136,8 @@ impl MinimumCutIntoBoundedSets { /// * `sink` - Sink vertex t (must be in V2) /// * `size_bound` - Maximum size B for each partition set /// - /// # Panics - /// Panics if edge_weights length doesn't match num_edges, if source == sink, + /// # Errors + /// Returns an error if edge_weights length doesn't match num_edges, if source == sink, /// or if source/sink are out of bounds. pub fn new( graph: G, @@ -130,22 +145,26 @@ impl MinimumCutIntoBoundedSets { source: usize, sink: usize, size_bound: usize, - ) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - assert!(source < graph.num_vertices(), "source vertex out of bounds"); - assert!(sink < graph.num_vertices(), "sink vertex out of bounds"); - assert_ne!(source, sink, "source and sink must be different vertices"); - Self { + ) -> Result { + if edge_weights.len() != graph.num_edges() { + return Err("edge_weights length must match num_edges".into()); + } + if !(source < graph.num_vertices()) { + return Err("source vertex out of bounds".into()); + } + if !(sink < graph.num_vertices()) { + return Err("sink vertex out of bounds".into()); + } + if source == sink { + return Err("source and sink must be different vertices".into()); + } + Ok(Self { graph, edge_weights, source, sink, size_bound, - } + }) } /// Get a reference to the underlying graph. @@ -245,8 +264,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -254,29 +277,33 @@ where pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "minimum_cut_into_bounded_sets", - instance: Box::new(MinimumCutIntoBoundedSets::new( - SimpleGraph::new( - 8, - vec![ - (0, 1), - (0, 2), - (1, 2), - (1, 3), - (2, 4), - (3, 5), - (3, 6), - (4, 5), - (4, 6), - (5, 7), - (6, 7), - (5, 6), - ], - ), - vec![2, 3, 1, 4, 2, 1, 3, 2, 1, 2, 3, 1], - 0, - 7, - 5, - )), + instance: Box::new( + MinimumCutIntoBoundedSets::new( + SimpleGraph::new( + 8, + vec![ + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (2, 4), + (3, 5), + (3, 6), + (4, 5), + (4, 6), + (5, 7), + (6, 7), + (5, 6), + ], + ) + .unwrap(), + vec![2, 3, 1, 4, 2, 1, 3, 2, 1, 2, 3, 1], + 0, + 7, + 5, + ) + .unwrap(), + ), // V1={0,1,2,3}, V2={4,5,6,7}: cut edges (2,4)=2,(3,5)=1,(3,6)=3 => 6 optimal_config: serde_json::json!(vec![false, false, false, false, true, true, true, true]), optimal_value: serde_json::json!(6), @@ -287,7 +314,7 @@ crate::impl_random_generate!(MinimumCutIntoBoundedSets, crate: let (source, sink) = spec.endpoints()?; let graph = spec.graph()?; let edge_weights = vec![1; graph.num_edges()]; - Ok(MinimumCutIntoBoundedSets::new(graph, edge_weights, source, sink, spec.num_vertices)) + Ok(MinimumCutIntoBoundedSets::new(graph, edge_weights, source, sink, spec.num_vertices).unwrap()) }); crate::declare_variants! { diff --git a/src/models/graph/minimum_dominating_set.rs b/src/models/graph/minimum_dominating_set.rs index b8c85a1a4..8d3db2478 100644 --- a/src/models/graph/minimum_dominating_set.rs +++ b/src/models/graph/minimum_dominating_set.rs @@ -43,8 +43,8 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Star graph: center dominates all -/// let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); -/// let problem = MinimumDominatingSet::new(graph, vec![1; 4]); +/// let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(); +/// let problem = MinimumDominatingSet::new(graph, vec![1; 4]).unwrap(); /// /// let solver = BruteForce::new(); /// let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -52,7 +52,7 @@ inventory::submit! { /// // Minimum dominating set is just the center vertex /// assert!(solutions.contains(&vec![true, false, false, false])); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumDominatingSet { /// The underlying graph. graph: G, @@ -60,6 +60,23 @@ pub struct MinimumDominatingSet { weights: Vec, } +#[derive(Deserialize)] +struct MinimumDominatingSetData { + graph: G, + weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MinimumDominatingSet +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumDominatingSetData::deserialize(deserializer)?; + Self::new(data.graph, data.weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumDominatingSetCreateSpec { /// The underlying graph G=(V,E). @@ -73,27 +90,17 @@ impl TryFrom> { type Error = crate::registry::ConstructionError; fn try_from(spec: MinimumDominatingSetCreateSpec) -> Result { - if spec.weights.len() != spec.graph.num_vertices() { - return Err(format!( - "weights has {} entries, expected {}", - spec.weights.len(), - spec.graph.num_vertices() - ) - .into()); - } - Ok(Self::new(spec.graph, spec.weights)) + Self::new(spec.graph, spec.weights) } } impl MinimumDominatingSet { /// Create a Dominating Set problem from a graph with given weights. - pub fn new(graph: G, weights: Vec) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - Self { graph, weights } + pub fn new(graph: G, weights: Vec) -> Result { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + Ok(Self { graph, weights }) } /// Get a reference to the underlying graph. @@ -205,16 +212,20 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } crate::impl_random_generate!(MinimumDominatingSet, crate::random::SimpleGraphRandomSpec, |spec| { - Ok(MinimumDominatingSet::new(spec.graph()?, vec![1; spec.num_vertices])) + MinimumDominatingSet::new(spec.graph()?, vec![1; spec.num_vertices]) }); crate::impl_random_generate!(MinimumDominatingSet, crate::random::SimpleGraphRandomSpec, |spec| { - Ok(MinimumDominatingSet::new(spec.graph()?, vec![One; spec.num_vertices])) + MinimumDominatingSet::new(spec.graph()?, vec![One; spec.num_vertices]) }); #[derive(Debug, Deserialize, crate::CreateSpec)] @@ -227,7 +238,7 @@ impl TryFrom for MinimumDominatingSet Result { let weights = vec![One; spec.graph.num_vertices()]; - Ok(Self::new(spec.graph, weights)) + Self::new(spec.graph, weights) } } @@ -308,10 +319,13 @@ crate::register_decision_variant!( pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "minimum_dominating_set_simplegraph", - instance: Box::new(MinimumDominatingSet::new( - SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]), - vec![1i64; 5], - )), + instance: Box::new( + MinimumDominatingSet::new( + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(), + vec![1i64; 5], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![false, false, true, true, false]), optimal_value: serde_json::json!(2), }] @@ -325,9 +339,11 @@ pub(crate) fn decision_canonical_model_example_specs( id: "decision_minimum_dominating_set_simplegraph", instance: Box::new(Decision::new( MinimumDominatingSet::new( - SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]) + .unwrap(), vec![1i64; 5], - ), + ) + .unwrap(), 2, )), optimal_config: serde_json::json!(vec![false, false, true, true, false]), @@ -340,9 +356,11 @@ pub(crate) fn decision_canonical_model_example_specs( SimpleGraph::new( 6, vec![(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (3, 5), (4, 5)], - ), + ) + .unwrap(), vec![One; 6], - ), + ) + .unwrap(), 2, )), optimal_config: serde_json::json!(vec![true, false, false, true, false, false]), @@ -364,9 +382,11 @@ pub(crate) fn decision_canonical_rule_example_specs( let source = crate::models::decision::Decision::new( MinimumDominatingSet::new( - SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]) + .unwrap(), vec![1i64; 5], - ), + ) + .unwrap(), 2, ); let result = source @@ -394,9 +414,11 @@ pub(crate) fn decision_canonical_rule_example_specs( let source = crate::models::decision::Decision::new( MinimumDominatingSet::new( - SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]) + .unwrap(), vec![One; 5], - ), + ) + .unwrap(), 2, ); let result = source diff --git a/src/models/graph/minimum_dummy_activities_pert.rs b/src/models/graph/minimum_dummy_activities_pert.rs index c272044d6..a18023ec0 100644 --- a/src/models/graph/minimum_dummy_activities_pert.rs +++ b/src/models/graph/minimum_dummy_activities_pert.rs @@ -11,6 +11,7 @@ use crate::registry::{CreateSpec, ProblemSchemaEntry}; use crate::topology::DirectedGraph; use crate::traits::Problem; use crate::types::Min; +use petgraph::unionfind::UnionFind; use serde::{Deserialize, Deserializer, Serialize}; use std::collections::{BTreeMap, BTreeSet}; @@ -61,10 +62,7 @@ impl TryFrom for MinimumDummyActivitiesPer .transpose()? .unwrap_or(0); let num_vertices = spec.num_vertices.unwrap_or(inferred); - if num_vertices < inferred { - return Err("num_vertices is too small for the provided arcs".into()); - } - Self::try_new(DirectedGraph::new(num_vertices, spec.arcs)) + Self::try_new(DirectedGraph::new(num_vertices, spec.arcs)?) } } @@ -161,7 +159,7 @@ impl MinimumDummyActivitiesPert { } let roots: Vec = (0..2 * num_tasks) - .map(|endpoint| uf.find(endpoint)) + .map(|endpoint| uf.find_mut(endpoint)) .collect(); let mut root_to_dense = BTreeMap::new(); for &root in &roots { @@ -206,7 +204,8 @@ impl MinimumDummyActivitiesPert { let mut event_arcs = task_arcs; event_arcs.extend(dummy_arcs.iter().copied()); - let event_graph = DirectedGraph::new(root_to_dense.len(), event_arcs); + let event_graph = DirectedGraph::new(root_to_dense.len(), event_arcs) + .expect("event arc endpoints are densely numbered"); if !event_graph.is_dag() { return None; } @@ -240,8 +239,12 @@ impl Problem for MinimumDummyActivitiesPert { } impl crate::solvers::BruteForceProblem for MinimumDummyActivitiesPert { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_arcs()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_arcs()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -257,10 +260,9 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "minimum_dummy_activities_pert", - instance: Box::new(MinimumDummyActivitiesPert::new(DirectedGraph::new( - 6, - vec![(0, 2), (0, 3), (1, 3), (1, 4), (2, 5)], - ))), + instance: Box::new(MinimumDummyActivitiesPert::new( + DirectedGraph::new(6, vec![(0, 2), (0, 3), (1, 3), (1, 4), (2, 5)]).unwrap(), + )), optimal_config: serde_json::json!(vec![true, false, false, true, true]), optimal_value: serde_json::json!(2), }] @@ -288,35 +290,6 @@ struct CandidatePertNetwork { num_dummy_arcs: usize, } -#[derive(Debug)] -struct UnionFind { - parent: Vec, -} - -impl UnionFind { - fn new(size: usize) -> Self { - Self { - parent: (0..size).collect(), - } - } - - fn find(&mut self, x: usize) -> usize { - if self.parent[x] != x { - let root = self.find(self.parent[x]); - self.parent[x] = root; - } - self.parent[x] - } - - fn union(&mut self, a: usize, b: usize) { - let root_a = self.find(a); - let root_b = self.find(b); - if root_a != root_b { - self.parent[root_b] = root_a; - } - } -} - fn start_endpoint(task: usize) -> usize { 2 * task } diff --git a/src/models/graph/minimum_edge_cost_flow.rs b/src/models/graph/minimum_edge_cost_flow.rs index 79e0ab8dd..929c6dacc 100644 --- a/src/models/graph/minimum_edge_cost_flow.rs +++ b/src/models/graph/minimum_edge_cost_flow.rs @@ -54,18 +54,19 @@ inventory::submit! { /// // 5-vertex network: s=0, t=4, R=3 /// let graph = DirectedGraph::new(5, vec![ /// (0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4), -/// ]); +/// ]).unwrap(); /// let problem = MinimumEdgeCostFlow::new( /// graph, /// vec![3, 1, 2, 0, 0, 0], // prices /// vec![2, 2, 2, 2, 2, 2], // capacities /// 0, 4, 3, -/// ); +/// ).unwrap(); /// let solver = BruteForce::new(); /// let witness = solver.solve(&problem).unwrap().unwrap(); /// assert_eq!(problem.evaluate(&witness).unwrap(), problemreductions::types::Min(Some(3))); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumEdgeCostFlowData")] pub struct MinimumEdgeCostFlow { /// The directed graph G = (V, A). graph: DirectedGraph, @@ -81,6 +82,30 @@ pub struct MinimumEdgeCostFlow { required_flow: i64, } +#[derive(Deserialize)] +struct MinimumEdgeCostFlowData { + graph: DirectedGraph, + prices: Vec, + capacities: Vec, + source: usize, + sink: usize, + required_flow: i64, +} + +impl TryFrom for MinimumEdgeCostFlow { + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumEdgeCostFlowData) -> Result { + Self::new( + data.graph, + data.prices, + data.capacities, + data.source, + data.sink, + data.required_flow, + ) + } +} + impl MinimumEdgeCostFlow { /// Create a new Minimum Edge-Cost Flow problem. /// @@ -93,9 +118,9 @@ impl MinimumEdgeCostFlow { /// * `sink` - Sink vertex index /// * `required_flow` - Minimum flow requirement R /// - /// # Panics + /// # Errors /// - /// Panics if: + /// Returns an error if: /// - `prices.len() != graph.num_arcs()` /// - `capacities.len() != graph.num_arcs()` /// - `source >= graph.num_vertices()` @@ -109,35 +134,43 @@ impl MinimumEdgeCostFlow { source: usize, sink: usize, required_flow: i64, - ) -> Self { + ) -> Result { let n = graph.num_vertices(); let m = graph.num_arcs(); - assert_eq!( - prices.len(), - m, - "prices length ({}) must match num_arcs ({m})", - prices.len() - ); - assert_eq!( - capacities.len(), - m, - "capacities length ({}) must match num_arcs ({m})", - capacities.len() - ); - assert!(source < n, "source ({source}) >= num_vertices ({n})"); - assert!(sink < n, "sink ({sink}) >= num_vertices ({n})"); - assert_ne!(source, sink, "source and sink must be distinct"); + if prices.len() != m { + return Err( + format!("prices length ({}) must match num_arcs ({m})", prices.len()).into(), + ); + } + if capacities.len() != m { + return Err(format!( + "capacities length ({}) must match num_arcs ({m})", + capacities.len() + ) + .into()); + } + if !(source < n) { + return Err(format!("source ({source}) >= num_vertices ({n})").into()); + } + if !(sink < n) { + return Err(format!("sink ({sink}) >= num_vertices ({n})").into()); + } + if source == sink { + return Err("source and sink must be distinct".into()); + } for (i, &c) in capacities.iter().enumerate() { - assert!(c >= 0, "capacity[{i}] = {c} is negative"); + if !(c >= 0) { + return Err(format!("capacity[{i}] = {c} is negative").into()); + } } - Self { + Ok(Self { graph, prices, capacities, source, sink, required_flow, - } + }) } /// Get a reference to the underlying directed graph. @@ -302,8 +335,12 @@ impl Problem for MinimumEdgeCostFlow { } impl crate::solvers::BruteForceProblem for MinimumEdgeCostFlow { - fn dimensions(&self) -> Vec { - self.capacities.iter().map(|&c| (c as usize) + 1).collect() + fn num_variables(&self) -> Result { + Ok(self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.capacities[variable]) + 1)?) } } @@ -319,17 +356,21 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "minimum_edge_cost_flow", - instance: Box::new(MinimumEdgeCostFlow::new( - crate::topology::DirectedGraph::new( - 5, - vec![(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4)], - ), - vec![3, 1, 2, 0, 0, 0], // prices - vec![2, 2, 2, 2, 2, 2], // capacities - 0, - 4, - 3, - )), + instance: Box::new( + MinimumEdgeCostFlow::new( + crate::topology::DirectedGraph::new( + 5, + vec![(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4)], + ) + .unwrap(), + vec![3, 1, 2, 0, 0, 0], // prices + vec![2, 2, 2, 2, 2, 2], // capacities + 0, + 4, + 3, + ) + .unwrap(), + ), // Optimal: route 1 unit via v2 and 2 units via v3 → cost = 1 + 2 = 3 // config = [0, 1, 2, 0, 1, 2] optimal_config: serde_json::json!(vec![0, 1, 2, 0, 1, 2]), diff --git a/src/models/graph/minimum_feedback_arc_set.rs b/src/models/graph/minimum_feedback_arc_set.rs index 7c337a1d5..ea8e9b1a8 100644 --- a/src/models/graph/minimum_feedback_arc_set.rs +++ b/src/models/graph/minimum_feedback_arc_set.rs @@ -45,8 +45,8 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Directed cycle: 0->1->2->0 -/// let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); -/// let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); +/// let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); +/// let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]).unwrap(); /// /// // Solve with brute force /// let solver = BruteForce::new(); @@ -55,7 +55,7 @@ inventory::submit! { /// // Minimum FAS has size 1 (remove any single arc to break the cycle) /// assert_eq!(solution.iter().filter(|&&selected| selected).count(), 1); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumFeedbackArcSet { /// The directed graph. graph: DirectedGraph, @@ -63,6 +63,22 @@ pub struct MinimumFeedbackArcSet { weights: Vec, } +#[derive(Deserialize)] +struct MinimumFeedbackArcSetData { + graph: DirectedGraph, + weights: Vec, +} + +impl<'de, W> Deserialize<'de> for MinimumFeedbackArcSet +where + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumFeedbackArcSetData::deserialize(deserializer)?; + Self::new(data.graph, data.weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumFeedbackArcSetCreateSpec { /// The directed graph. @@ -75,22 +91,18 @@ impl TryFrom for MinimumFeedbackArcSet { fn try_from(spec: MinimumFeedbackArcSetCreateSpec) -> Result { let count = spec.graph.num_arcs(); let weights = spec.weights.unwrap_or_else(|| vec![1; count]); - if weights.len() != count { - return Err(format!("weights has {} entries, expected {count}", weights.len()).into()); - } - Ok(Self::new(spec.graph, weights)) + Self::new(spec.graph, weights) } } impl MinimumFeedbackArcSet { /// Create a Minimum Feedback Arc Set problem from a directed graph with given weights. - pub fn new(graph: DirectedGraph, weights: Vec) -> Self { - assert_eq!( - weights.len(), - graph.num_arcs(), - "weights length must match graph num_arcs" - ); - Self { graph, weights } + pub fn new( + graph: DirectedGraph, + weights: Vec, + ) -> Result { + Self::check_weights(&graph, &weights)?; + Ok(Self { graph, weights }) } /// Get a reference to the underlying directed graph. @@ -104,13 +116,23 @@ impl MinimumFeedbackArcSet { } /// Set arc weights. - pub fn set_weights(&mut self, weights: Vec) { - assert_eq!( - weights.len(), - self.graph.num_arcs(), - "weights length must match graph num_arcs" - ); + pub fn set_weights( + &mut self, + weights: Vec, + ) -> Result<(), crate::registry::ConstructionError> { + Self::check_weights(&self.graph, &weights)?; self.weights = weights; + Ok(()) + } + + fn check_weights( + graph: &DirectedGraph, + weights: &[W], + ) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_arcs() { + return Err("weights length must match graph num_arcs".into()); + } + Ok(()) } /// Check if a configuration is a valid feedback arc set. @@ -184,8 +206,12 @@ impl crate::solvers::BruteForceProblem for MinimumFeedbackArcSet where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_arcs()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_arcs()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -217,10 +243,13 @@ pub(crate) fn canonical_model_example_specs() -> Vec { /// The underlying directed graph. graph: DirectedGraph, @@ -57,6 +57,22 @@ pub struct MinimumFeedbackVertexSet { weights: Vec, } +#[derive(Deserialize)] +struct MinimumFeedbackVertexSetData { + graph: DirectedGraph, + weights: Vec, +} + +impl<'de, W> Deserialize<'de> for MinimumFeedbackVertexSet +where + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumFeedbackVertexSetData::deserialize(deserializer)?; + Self::new(data.graph, data.weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumFeedbackVertexSetCreateSpec { /// The directed graph. @@ -71,22 +87,18 @@ impl TryFrom> fn try_from(spec: MinimumFeedbackVertexSetCreateSpec) -> Result { let count = spec.graph.num_vertices(); let weights = spec.weights.unwrap_or_else(|| vec![W::unit(); count]); - if weights.len() != count { - return Err(format!("weights has {} entries, expected {count}", weights.len()).into()); - } - Ok(Self::new(spec.graph, weights)) + Self::new(spec.graph, weights) } } impl MinimumFeedbackVertexSet { /// Create a Feedback Vertex Set problem from a directed graph with given weights. - pub fn new(graph: DirectedGraph, weights: Vec) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - Self { graph, weights } + pub fn new( + graph: DirectedGraph, + weights: Vec, + ) -> Result { + Self::check_weights(&graph, &weights)?; + Ok(Self { graph, weights }) } /// Get a reference to the underlying directed graph. @@ -100,13 +112,23 @@ impl MinimumFeedbackVertexSet { } /// Set vertex weights. - pub fn set_weights(&mut self, weights: Vec) { - assert_eq!( - weights.len(), - self.graph.num_vertices(), - "weights length must match graph num_vertices" - ); + pub fn set_weights( + &mut self, + weights: Vec, + ) -> Result<(), crate::registry::ConstructionError> { + Self::check_weights(&self.graph, &weights)?; self.weights = weights; + Ok(()) + } + + fn check_weights( + graph: &DirectedGraph, + weights: &[W], + ) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + Ok(()) } /// Check if a configuration is a valid feedback vertex set. @@ -185,8 +207,12 @@ impl crate::solvers::BruteForceProblem for MinimumFeedbackVertexSet where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -200,7 +226,7 @@ impl TryFrom for MinimumFeedbackVertexSet type Error = crate::registry::ConstructionError; fn try_from(spec: MinimumFeedbackVertexSetOneCreateSpec) -> Result { let weights = vec![One; spec.graph.num_vertices()]; - Ok(Self::new(spec.graph, weights)) + Self::new(spec.graph, weights) } } @@ -220,22 +246,29 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec { - vec![2; self.num_points()] + fn num_variables(&self) -> Result { + Ok(self.num_points()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/minimum_graph_bandwidth.rs b/src/models/graph/minimum_graph_bandwidth.rs index d214d03e8..b463aeca4 100644 --- a/src/models/graph/minimum_graph_bandwidth.rs +++ b/src/models/graph/minimum_graph_bandwidth.rs @@ -51,7 +51,7 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Star graph S4: center 0 connected to 1, 2, 3 -/// let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); +/// let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(); /// let problem = MinimumGraphBandwidth::new(graph); /// /// let solver = BruteForce::new(); @@ -170,9 +170,12 @@ impl crate::solvers::BruteForceProblem for MinimumGraphBandwidth where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } @@ -194,10 +197,9 @@ pub(crate) fn canonical_model_example_specs() -> Vec crate::solvers::BruteForceProblem for MinimumIntersectionGraphBasis where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - let m = self.graph.num_edges(); - if m == 0 { - // No edges: no variables needed; empty assignment is trivially valid. - return vec![]; - } - vec![2; n * m] + fn num_variables(&self) -> Result { + (self.graph.num_vertices()) + .checked_mul(self.graph.num_edges()) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a search coordinate size".into(), + ) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2) } } @@ -195,10 +199,9 @@ pub(crate) fn canonical_model_example_specs() -> Vec crate::solvers::BruteForceProblem for MinimumMaximalMatching where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -187,10 +191,9 @@ pub(crate) fn canonical_model_example_specs() -> Vec(graph: &G, source: usize) -> Vec { /// use problemreductions::{Problem, BruteForce}; /// /// // House graph: vertices 0–4 -/// let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); +/// let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(); /// let problem = MinimumMetricDimension::new(graph); /// /// let solver = BruteForce::new(); @@ -190,8 +190,12 @@ impl crate::solvers::BruteForceProblem for MinimumMetricDimension where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -207,10 +211,9 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "minimum_metric_dimension_simplegraph", - instance: Box::new(MinimumMetricDimension::new(SimpleGraph::new( - 5, - vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)], - ))), + instance: Box::new(MinimumMetricDimension::new( + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(), + )), optimal_config: serde_json::json!(vec![true, true, false, false, false]), optimal_value: serde_json::json!(2), }] diff --git a/src/models/graph/minimum_multiway_cut.rs b/src/models/graph/minimum_multiway_cut.rs index 6b1b66cec..5783eb917 100644 --- a/src/models/graph/minimum_multiway_cut.rs +++ b/src/models/graph/minimum_multiway_cut.rs @@ -42,13 +42,32 @@ inventory::submit! { /// /// A configuration is feasible if removing the cut edges disconnects all /// terminal pairs. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumMultiwayCut { graph: G, terminals: Vec, edge_weights: Vec, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))] +struct MinimumMultiwayCutData { + graph: G, + terminals: Vec, + edge_weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MinimumMultiwayCut +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumMultiwayCutData::::deserialize(deserializer)?; + Self::new(data.graph, data.terminals, data.edge_weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumMultiwayCutCreateSpec { /// The undirected graph G=(V,E). @@ -62,35 +81,7 @@ struct MinimumMultiwayCutCreateSpec { impl TryFrom for MinimumMultiwayCut { type Error = crate::registry::ConstructionError; fn try_from(spec: MinimumMultiwayCutCreateSpec) -> Result { - if spec.edge_weights.len() != spec.graph.num_edges() { - return Err(format!( - "edge_weights has {} entries, expected {}", - spec.edge_weights.len(), - spec.graph.num_edges() - ) - .into()); - } - if spec.terminals.len() < 2 { - return Err("at least two terminals are required".to_string().into()); - } - let mut distinct = spec.terminals.clone(); - distinct.sort_unstable(); - distinct.dedup(); - if distinct.len() != spec.terminals.len() { - return Err("terminals must be distinct".to_string().into()); - } - if let Some(&terminal) = spec - .terminals - .iter() - .find(|&&t| t >= spec.graph.num_vertices()) - { - return Err(format!( - "terminal {terminal} is outside graph with {} vertices", - spec.graph.num_vertices() - ) - .into()); - } - Ok(Self::new(spec.graph, spec.terminals, spec.edge_weights)) + Self::new(spec.graph, spec.terminals, spec.edge_weights) } } @@ -101,30 +92,38 @@ impl MinimumMultiwayCut { /// [`Graph::edges()`](crate::topology::Graph::edges). Each binary /// variable corresponds to an edge: 0 = keep, 1 = cut. /// - /// # Panics + /// # Errors /// - If `edge_weights.len() != graph.num_edges()` /// - If `terminals.len() < 2` /// - If any terminal index is out of bounds /// - If there are duplicate terminal indices - pub fn new(graph: G, terminals: Vec, edge_weights: Vec) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - assert!(terminals.len() >= 2, "need at least 2 terminals"); + pub fn new( + graph: G, + terminals: Vec, + edge_weights: Vec, + ) -> Result { + if edge_weights.len() != graph.num_edges() { + return Err("edge_weights length must match num_edges".into()); + } + if !(terminals.len() >= 2) { + return Err("need at least 2 terminals".into()); + } let mut sorted = terminals.clone(); sorted.sort(); sorted.dedup(); - assert_eq!(sorted.len(), terminals.len(), "duplicate terminal indices"); + if sorted.len() != terminals.len() { + return Err("duplicate terminal indices".into()); + } for &t in &terminals { - assert!(t < graph.num_vertices(), "terminal index out of bounds"); + if !(t < graph.num_vertices()) { + return Err("terminal index out of bounds".into()); + } } - Self { + Ok(Self { graph, terminals, edge_weights, - } + }) } /// Get a reference to the underlying graph. @@ -251,8 +250,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -268,11 +271,14 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "minimum_multiway_cut_simplegraph", - instance: Box::new(MinimumMultiwayCut::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]), - vec![0, 2, 4], - vec![2, 3, 1, 2, 4, 5], - )), + instance: Box::new( + MinimumMultiwayCut::new( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]).unwrap(), + vec![0, 2, 4], + vec![2, 3, 1, 2, 4, 5], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![true, false, false, true, true, false]), optimal_value: serde_json::json!(8), }] diff --git a/src/models/graph/minimum_sum_multicenter.rs b/src/models/graph/minimum_sum_multicenter.rs index 2ba567460..c44686b5a 100644 --- a/src/models/graph/minimum_sum_multicenter.rs +++ b/src/models/graph/minimum_sum_multicenter.rs @@ -46,15 +46,15 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Path graph: 0-1-2, unit weights and lengths, K=1 -/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); -/// let problem = MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 1); +/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); +/// let problem = MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 1).unwrap(); /// /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap().unwrap(); /// // Center at vertex 1 gives total distance 0+1+1 = 2 (optimal) /// assert_eq!(solution, vec![false, true, false]); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumSumMulticenter { /// The underlying graph. graph: G, @@ -66,6 +66,27 @@ pub struct MinimumSumMulticenter { k: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))] +struct MinimumSumMulticenterData { + graph: G, + vertex_weights: Vec, + edge_lengths: Vec, + k: usize, +} + +impl<'de, G, W> Deserialize<'de> for MinimumSumMulticenter +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumSumMulticenterData::::deserialize(deserializer)?; + Self::new(data.graph, data.vertex_weights, data.edge_lengths, data.k) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumSumMulticenterCreateSpec { #[create(codec = "edge-list")] @@ -98,29 +119,10 @@ impl TryFrom for MinimumSumMulticenter graph.num_vertices() { - return Err(format!("k must be between 1 and {}", graph.num_vertices()).into()); - } - Ok(Self::new(graph, vertex_weights, edge_lengths, spec.k)) + Self::new(graph, vertex_weights, edge_lengths, spec.k) } } @@ -149,35 +151,40 @@ fn simple_graph_from_create( if num_vertices < inferred { return Err(format!("num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}").into()); } - Ok(SimpleGraph::new(num_vertices, edges)) + SimpleGraph::new(num_vertices, edges) } impl MinimumSumMulticenter { /// Create a MinimumSumMulticenter problem. /// - /// # Panics + /// # Errors /// - If `vertex_weights.len() != graph.num_vertices()` /// - If `edge_lengths.len() != graph.num_edges()` /// - If `k == 0` or `k > graph.num_vertices()` - pub fn new(graph: G, vertex_weights: Vec, edge_lengths: Vec, k: usize) -> Self { - assert_eq!( - vertex_weights.len(), - graph.num_vertices(), - "vertex_weights length must match num_vertices" - ); - assert_eq!( - edge_lengths.len(), - graph.num_edges(), - "edge_lengths length must match num_edges" - ); - assert!(k > 0, "k must be positive"); - assert!(k <= graph.num_vertices(), "k must not exceed num_vertices"); - Self { + pub fn new( + graph: G, + vertex_weights: Vec, + edge_lengths: Vec, + k: usize, + ) -> Result { + if vertex_weights.len() != graph.num_vertices() { + return Err("vertex_weights length must match num_vertices".into()); + } + if edge_lengths.len() != graph.num_edges() { + return Err("edge_lengths length must match num_edges".into()); + } + if k == 0 { + return Err("k must be positive".into()); + } + if !(k <= graph.num_vertices()) { + return Err("k must not exceed num_vertices".into()); + } + Ok(Self { graph, vertex_weights, edge_lengths, k, - } + }) } /// Get a reference to the underlying graph. @@ -352,8 +359,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -368,7 +379,7 @@ crate::impl_random_generate!(MinimumSumMulticenter, MinimumSum return Err(format!("k must be between 1 and {}", spec.num_vertices).into()); } let lengths = vec![1; graph.num_edges()]; - Ok(MinimumSumMulticenter::new(graph, vec![1; spec.num_vertices], lengths, k)) + MinimumSumMulticenter::new(graph, vec![1; spec.num_vertices], lengths, k) }); crate::declare_variants! { @@ -383,24 +394,28 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "minimum_sum_multicenter_simplegraph", - instance: Box::new(MinimumSumMulticenter::new( - SimpleGraph::new( - 7, - vec![ - (0, 1), - (1, 2), - (2, 3), - (3, 4), - (4, 5), - (5, 6), - (0, 6), - (2, 5), - ], - ), - vec![1i64; 7], - vec![1i64; 8], - 2, - )), + instance: Box::new( + MinimumSumMulticenter::new( + SimpleGraph::new( + 7, + vec![ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), + (5, 6), + (0, 6), + (2, 5), + ], + ) + .unwrap(), + vec![1i64; 7], + vec![1i64; 8], + 2, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![false, false, true, false, false, true, false]), optimal_value: serde_json::json!(6), }] diff --git a/src/models/graph/minimum_vertex_cover.rs b/src/models/graph/minimum_vertex_cover.rs index 5f511118b..93c4f57d8 100644 --- a/src/models/graph/minimum_vertex_cover.rs +++ b/src/models/graph/minimum_vertex_cover.rs @@ -42,8 +42,8 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Create a path graph 0-1-2 -/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); -/// let problem = MinimumVertexCover::new(graph, vec![1; 3]); +/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); +/// let problem = MinimumVertexCover::new(graph, vec![1; 3]).unwrap(); /// /// // Solve with brute force /// let solver = BruteForce::new(); @@ -52,7 +52,7 @@ inventory::submit! { /// // Minimum vertex cover is just vertex 1 /// assert!(solutions.contains(&vec![false, true, false])); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumVertexCover { /// The underlying graph. graph: G, @@ -60,6 +60,23 @@ pub struct MinimumVertexCover { weights: Vec, } +#[derive(Deserialize)] +struct MinimumVertexCoverData { + graph: G, + weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MinimumVertexCover +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumVertexCoverData::deserialize(deserializer)?; + Self::new(data.graph, data.weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumVertexCoverCreateSpec { /// The underlying graph G=(V,E). @@ -76,27 +93,17 @@ impl TryFrom> let weights = spec .weights .unwrap_or_else(|| vec![W::unit(); spec.graph.num_vertices()]); - if weights.len() != spec.graph.num_vertices() { - return Err(format!( - "weights has {} entries, expected {}", - weights.len(), - spec.graph.num_vertices() - ) - .into()); - } - Ok(Self::new(spec.graph, weights)) + Self::new(spec.graph, weights) } } impl MinimumVertexCover { /// Create a Vertex Covering problem from a graph with given weights. - pub fn new(graph: G, weights: Vec) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - Self { graph, weights } + pub fn new(graph: G, weights: Vec) -> Result { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + Ok(Self { graph, weights }) } /// Get a reference to the underlying graph. @@ -183,8 +190,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -201,10 +212,10 @@ pub(crate) fn is_vertex_cover_config(graph: &G, config: &[bool]) -> bo } crate::impl_random_generate!(MinimumVertexCover, crate::random::SimpleGraphRandomSpec, |spec| { - Ok(MinimumVertexCover::new(spec.graph()?, vec![1; spec.num_vertices])) + MinimumVertexCover::new(spec.graph()?, vec![1; spec.num_vertices]) }); crate::impl_random_generate!(MinimumVertexCover, crate::random::SimpleGraphRandomSpec, |spec| { - Ok(MinimumVertexCover::new(spec.graph()?, vec![One; spec.num_vertices])) + MinimumVertexCover::new(spec.graph()?, vec![One; spec.num_vertices]) }); #[derive(Debug, Deserialize, crate::CreateSpec)] @@ -217,7 +228,7 @@ impl TryFrom for MinimumVertexCover Result { let weights = vec![One; spec.graph.num_vertices()]; - Ok(Self::new(spec.graph, weights)) + Self::new(spec.graph, weights) } } @@ -240,7 +251,11 @@ where const DECISION_NAME: &'static str = "DecisionMinimumVertexCover"; } -impl Decision> { +impl Decision> +where + W: WeightElement + crate::variant::VariantParam, + W::Sum: std::fmt::Debug + serde::Serialize + serde::de::DeserializeOwned, +{ /// Number of vertices in the underlying graph. pub fn num_vertices(&self) -> usize { self.inner().num_vertices() @@ -250,11 +265,6 @@ impl Decision> { pub fn num_edges(&self) -> usize { self.inner().num_edges() } - - /// Decision bound as a nonnegative integer. - pub fn k(&self) -> usize { - (*self.bound()).try_into().unwrap_or(0) - } } #[derive(Debug, Deserialize, crate::CreateSpec)] @@ -283,7 +293,7 @@ crate::impl_random_generate!( } .graph()?; Ok(Decision::new( - MinimumVertexCover::new(graph, vec![1; spec.num_vertices]), + MinimumVertexCover::new(graph, vec![1; spec.num_vertices])?, spec.bound, )) } @@ -298,13 +308,14 @@ crate::register_decision_variant!( category: crate::registry::ProblemCategory::Graph, dims: [ VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), - VariantDimension::new("weight", "i64", &["i64"]), + VariantDimension::new("weight", "i64", &["i64", "One"]), ], fields: [ FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" }, FieldInfo { name: "weights", type_name: "Vec", description: "Vertex weights w: V -> R" }, FieldInfo { name: "bound", type_name: "W::Sum", description: "Decision bound (maximum allowed cover cost)" }, ], + additional: [MinimumVertexCover => "1.1996^num_vertices"], decode: |_, indices: Vec| crate::config::config_to_bits(&indices), random ); @@ -313,10 +324,13 @@ crate::register_decision_variant!( pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "minimum_vertex_cover_simplegraph", - instance: Box::new(MinimumVertexCover::new( - SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]), - vec![1i64; 5], - )), + instance: Box::new( + MinimumVertexCover::new( + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(), + vec![1i64; 5], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![true, false, false, true, true]), optimal_value: serde_json::json!(3), }] @@ -325,52 +339,75 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec { - vec![crate::example_db::specs::ModelExampleSpec { - id: "decision_minimum_vertex_cover_simplegraph", - instance: Box::new(crate::models::decision::Decision::new( - MinimumVertexCover::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2), (2, 3)]), - vec![1i64; 4], - ), - 2, - )), - optimal_config: serde_json::json!(vec![true, false, true, false]), - optimal_value: serde_json::json!(true), - }] + vec![ + crate::example_db::specs::ModelExampleSpec { + id: "decision_minimum_vertex_cover_simplegraph", + instance: Box::new(crate::models::decision::Decision::new( + MinimumVertexCover::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2), (2, 3)]).unwrap(), + vec![1i64; 4], + ) + .unwrap(), + 2, + )), + optimal_config: serde_json::json!(vec![true, false, true, false]), + optimal_value: serde_json::json!(true), + }, + crate::example_db::specs::ModelExampleSpec { + id: "decision_minimum_vertex_cover_unit", + instance: Box::new(Decision::new( + MinimumVertexCover::new(SimpleGraph::path(3), vec![One; 3]).unwrap(), + 1, + )), + optimal_config: serde_json::json!([false, true, false]), + optimal_value: serde_json::json!(true), + }, + ] } #[cfg(feature = "example-db")] pub(crate) fn decision_canonical_rule_example_specs( ) -> Vec { - vec![crate::example_db::specs::RuleExampleSpec { - id: "decision_minimum_vertex_cover_to_minimum_vertex_cover", - build: || { - use crate::example_db::specs::assemble_rule_example; - use crate::export::SolutionPair; - use crate::rules::{AggregateReductionResult, ReduceToAggregate}; - - let source = crate::models::decision::Decision::new( - MinimumVertexCover::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2), (2, 3)]), - vec![1i64; 4], - ), - 2, - ); - let result = source - .reduce_to_aggregate() - .expect("reduction should succeed"); - let target = result.target_problem(); - let config = vec![true, false, true, false]; - assemble_rule_example( - &source, - target, - vec![SolutionPair { - source_config: serde_json::json!(config.clone()), - target_config: serde_json::json!(config), - }], - ) + use crate::example_db::specs::{rule_example_with_witness, RuleExampleSpec}; + use crate::export::SolutionPair; + vec![ + RuleExampleSpec { + id: "decision_minimum_vertex_cover_to_minimum_vertex_cover", + build: || { + let source = Decision::new( + MinimumVertexCover::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2), (2, 3)]).unwrap(), + vec![1i64; 4], + ) + .unwrap(), + 2, + ); + rule_example_with_witness::<_, MinimumVertexCover>( + source, + SolutionPair { + source_config: serde_json::json!([true, false, true, false]), + target_config: serde_json::json!([true, false, true, false]), + }, + ) + }, }, - }] + RuleExampleSpec { + id: "decision_minimum_vertex_cover_unit_to_minimum_vertex_cover", + build: || { + let source = Decision::new( + MinimumVertexCover::new(SimpleGraph::path(3), vec![One; 3]).unwrap(), + 1, + ); + rule_example_with_witness::<_, MinimumVertexCover>( + source, + SolutionPair { + source_config: serde_json::json!([false, true, false]), + target_config: serde_json::json!([false, true, false]), + }, + ) + }, + }, + ] } /// Check if a set of vertices forms a vertex cover. diff --git a/src/models/graph/mixed_chinese_postman.rs b/src/models/graph/mixed_chinese_postman.rs index 8bd02cb1e..15a37ba78 100644 --- a/src/models/graph/mixed_chinese_postman.rs +++ b/src/models/graph/mixed_chinese_postman.rs @@ -90,22 +90,12 @@ macro_rules! mixed_chinese_postman_create_spec { .transpose()? .unwrap_or(0); let num_vertices = spec.num_vertices.unwrap_or(inferred); - if num_vertices < inferred { - return Err(format!( - "num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}" - ).into()); - } - for (index, &(u, v)) in spec.arcs.iter().enumerate() { - if u >= num_vertices || v >= num_vertices { - return Err(format!( - "arc {index} endpoint is out of range for {num_vertices} vertices" - ).into()); - } - } + + let arc_weights = { $(if let Some(value) = spec.$arc_weights { value } else)? { vec![$one; spec.arcs.len()] } }; let edge_weights = { $(if let Some(value) = spec.$edge_weights { value } else)? { vec![$one; spec.graph.len()] } }; MixedChinesePostman::try_new( - MixedGraph::new(num_vertices, spec.arcs, spec.graph), + MixedGraph::new(num_vertices, spec.arcs, spec.graph)?, arc_weights, edge_weights, ) @@ -297,6 +287,7 @@ where keep[head] = true; } if !DirectedGraph::new(self.graph.num_vertices(), available) + .expect("available arcs use vertices of the input graph") .induced_subgraph(&keep) .is_strongly_connected() { @@ -349,8 +340,12 @@ impl crate::solvers::BruteForceProblem for MixedChinesePostman where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -373,7 +368,8 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec crate::solvers::BruteForceProblem for MonochromaticTriangle where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.edge_list.len()] + fn num_variables(&self) -> Result { + Ok(self.edge_list.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -212,10 +216,9 @@ pub(crate) fn canonical_model_example_specs() -> Vec colors 1,0,1 -> not monochromatic vec![crate::example_db::specs::ModelExampleSpec { id: "monochromatic_triangle_simplegraph", - instance: Box::new(MonochromaticTriangle::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - ))), + instance: Box::new(MonochromaticTriangle::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), + )), optimal_config: serde_json::json!(vec![false, false, true, true, false, true]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/multiple_choice_branching.rs b/src/models/graph/multiple_choice_branching.rs index b04671ee2..9ceb1efa2 100644 --- a/src/models/graph/multiple_choice_branching.rs +++ b/src/models/graph/multiple_choice_branching.rs @@ -71,12 +71,7 @@ impl TryFrom for MultipleChoiceBranching .transpose()? .unwrap_or(0); let num_vertices = spec.num_vertices.unwrap_or(inferred); - if num_vertices < inferred { - return Err("num_vertices is too small for arc endpoints" - .to_string() - .into()); - } - let graph = DirectedGraph::new(num_vertices, spec.arcs); + let graph = DirectedGraph::new(num_vertices, spec.arcs)?; let num_arcs = graph.num_arcs(); if spec.weights.len() != num_arcs { return Err(format!( @@ -268,8 +263,12 @@ impl crate::solvers::BruteForceProblem for MultipleChoiceBranching where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_arcs()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_arcs()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -387,7 +386,8 @@ pub(crate) fn canonical_model_example_specs() -> Vec, storage: Vec, } +#[derive(Deserialize)] +struct MultipleCopyFileAllocationData { + graph: SimpleGraph, + usage: Vec, + storage: Vec, +} + +impl TryFrom for MultipleCopyFileAllocation { + type Error = crate::registry::ConstructionError; + fn try_from(data: MultipleCopyFileAllocationData) -> Result { + Self::new(data.graph, data.usage, data.storage) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MultipleCopyFileAllocationCreateSpec { /// Network graph edges. @@ -74,41 +89,32 @@ impl TryFrom for MultipleCopyFileAllocatio .transpose()? .unwrap_or(0); let count = spec.num_vertices.unwrap_or(inferred); - if count < inferred { - return Err("num_vertices is too small for graph endpoints".into()); - } - if spec.usage.len() != count { - return Err("usage length must match num_vertices".into()); - } - if spec.storage.len() != count { - return Err("storage length must match num_vertices".into()); - } - Ok(Self { - graph: SimpleGraph::new(count, spec.graph), - usage: spec.usage, - storage: spec.storage, - }) + Self::new( + SimpleGraph::new(count, spec.graph)?, + spec.usage, + spec.storage, + ) } } impl MultipleCopyFileAllocation { /// Create a new Multiple Copy File Allocation instance. - pub fn new(graph: SimpleGraph, usage: Vec, storage: Vec) -> Self { - assert_eq!( - usage.len(), - graph.num_vertices(), - "usage length must match graph num_vertices" - ); - assert_eq!( - storage.len(), - graph.num_vertices(), - "storage length must match graph num_vertices" - ); - Self { + pub fn new( + graph: SimpleGraph, + usage: Vec, + storage: Vec, + ) -> Result { + if usage.len() != graph.num_vertices() { + return Err("usage length must match graph num_vertices".into()); + } + if storage.len() != graph.num_vertices() { + return Err("storage length must match graph num_vertices".into()); + } + Ok(Self { graph, usage, storage, - } + }) } /// Get a reference to the underlying graph. @@ -270,8 +276,12 @@ impl Problem for MultipleCopyFileAllocation { } impl crate::solvers::BruteForceProblem for MultipleCopyFileAllocation { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -279,11 +289,14 @@ impl crate::solvers::BruteForceProblem for MultipleCopyFileAllocation { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "multiple_copy_file_allocation", - instance: Box::new(MultipleCopyFileAllocation::new( - SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]), - vec![5, 1, 1, 1, 1, 5], - vec![6, 2, 6, 6, 2, 6], - )), + instance: Box::new( + MultipleCopyFileAllocation::new( + SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]).unwrap(), + vec![5, 1, 1, 1, 1, 5], + vec![6, 2, 6, 6, 2, 6], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![false, true, false, false, true, false]), optimal_value: serde_json::json!(16), }] diff --git a/src/models/graph/optimal_linear_arrangement.rs b/src/models/graph/optimal_linear_arrangement.rs index c50552603..597516765 100644 --- a/src/models/graph/optimal_linear_arrangement.rs +++ b/src/models/graph/optimal_linear_arrangement.rs @@ -54,7 +54,7 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Path graph: 0-1-2-3 -/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); +/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); /// let problem = OptimalLinearArrangement::new(graph); /// /// let solver = BruteForce::new(); @@ -184,9 +184,12 @@ impl crate::solvers::BruteForceProblem for OptimalLinearArrangement where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } @@ -252,10 +255,13 @@ pub(crate) fn canonical_model_example_specs() -> Vec crate::solvers::BruteForceProblem for PartialFeedbackEdgeSet where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -248,7 +252,8 @@ pub(crate) fn canonical_model_example_specs() -> Vec = [(0, 2), (2, 3), (3, 4)] .into_iter() .map(|(u, v)| normalize_edge(u, v)) diff --git a/src/models/graph/partition_into_cliques.rs b/src/models/graph/partition_into_cliques.rs index 5bb87de80..dd0e8bef6 100644 --- a/src/models/graph/partition_into_cliques.rs +++ b/src/models/graph/partition_into_cliques.rs @@ -46,14 +46,14 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Two triangles: 0-1-2-0 and 3-4-5-3 -/// let graph = SimpleGraph::new(6, vec![(0,1),(0,2),(1,2),(3,4),(3,5),(4,5)]); -/// let problem = PartitionIntoCliques::new(graph, 3); +/// let graph = SimpleGraph::new(6, vec![(0,1),(0,2),(1,2),(3,4),(3,5),(4,5)]).unwrap(); +/// let problem = PartitionIntoCliques::new(graph, 3).unwrap(); /// /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct PartitionIntoCliques { /// The underlying graph. @@ -62,18 +62,36 @@ pub struct PartitionIntoCliques { num_cliques: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct PartitionIntoCliquesData { + graph: G, + num_cliques: usize, +} + +impl<'de, G> Deserialize<'de> for PartitionIntoCliques +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = PartitionIntoCliquesData::::deserialize(deserializer)?; + Self::new(data.graph, data.num_cliques).map_err(serde::de::Error::custom) + } +} + impl PartitionIntoCliques { /// Create a new Partition Into Cliques instance. /// - /// # Panics - /// Panics if `num_cliques` is zero or greater than `graph.num_vertices()`. - pub fn new(graph: G, num_cliques: usize) -> Self { - assert!(num_cliques >= 1, "num_cliques must be at least 1"); - assert!( - num_cliques <= graph.num_vertices(), - "num_cliques must be at most num_vertices" - ); - Self { graph, num_cliques } + /// # Errors + /// Returns an error if `num_cliques` is zero or greater than `graph.num_vertices()`. + pub fn new(graph: G, num_cliques: usize) -> Result { + if num_cliques == 0 { + return Err("num_cliques must be at least 1".into()); + } + if !(num_cliques <= graph.num_vertices()) { + return Err("num_cliques must be at most num_vertices".into()); + } + Ok(Self { graph, num_cliques }) } /// Get a reference to the underlying graph. @@ -139,8 +157,12 @@ impl crate::solvers::BruteForceProblem for PartitionIntoCliques where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.num_cliques; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_cliques) } } @@ -183,23 +205,27 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "partition_into_cliques_simplegraph", - instance: Box::new(PartitionIntoCliques::new( - SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (1, 2), - (3, 4), - (3, 5), - (4, 5), - (0, 3), - (1, 4), - (2, 5), - ], - ), - 3, - )), + instance: Box::new( + PartitionIntoCliques::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 2), + (3, 4), + (3, 5), + (4, 5), + (0, 3), + (1, 4), + (2, 5), + ], + ) + .unwrap(), + 3, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![0, 0, 0, 1, 1, 1]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/partition_into_forests.rs b/src/models/graph/partition_into_forests.rs index 09fe3a156..3fa483d2b 100644 --- a/src/models/graph/partition_into_forests.rs +++ b/src/models/graph/partition_into_forests.rs @@ -8,6 +8,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; use crate::variant::VariantParam; +use petgraph::unionfind::UnionFind; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -46,14 +47,14 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Graph containing two triangles; K=2 forests suffice -/// let graph = SimpleGraph::new(6, vec![(0,1),(1,2),(2,0),(2,3),(3,4),(4,5),(5,3)]); -/// let problem = PartitionIntoForests::new(graph, 2); +/// let graph = SimpleGraph::new(6, vec![(0,1),(1,2),(2,0),(2,3),(3,4),(4,5),(5,3)]).unwrap(); +/// let problem = PartitionIntoForests::new(graph, 2).unwrap(); /// /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct PartitionIntoForests { /// The underlying graph. @@ -62,14 +63,33 @@ pub struct PartitionIntoForests { num_forests: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct PartitionIntoForestsData { + graph: G, + num_forests: usize, +} + +impl<'de, G> Deserialize<'de> for PartitionIntoForests +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = PartitionIntoForestsData::::deserialize(deserializer)?; + Self::new(data.graph, data.num_forests).map_err(serde::de::Error::custom) + } +} + impl PartitionIntoForests { /// Create a new Partition Into Forests instance. /// - /// # Panics - /// Panics if `num_forests` is zero. - pub fn new(graph: G, num_forests: usize) -> Self { - assert!(num_forests >= 1, "num_forests must be at least 1"); - Self { graph, num_forests } + /// # Errors + /// Returns an error if `num_forests` is zero. + pub fn new(graph: G, num_forests: usize) -> Result { + if num_forests == 0 { + return Err("num_forests must be at least 1".into()); + } + Ok(Self { graph, num_forests }) } /// Get a reference to the underlying graph. @@ -139,8 +159,12 @@ impl crate::solvers::BruteForceProblem for PartitionIntoForests where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.num_forests; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_forests) } } @@ -159,14 +183,7 @@ fn is_valid_forest_partition(graph: &G, num_forests: usize, config: &[ // For each forest class, verify the induced subgraph is acyclic using union-find. // An undirected graph is acyclic iff union-find never sees an edge (u, v) where // u and v already share a component. - let mut parent: Vec = (0..n).collect(); - - fn find(parent: &mut Vec, x: usize) -> usize { - if parent[x] != x { - parent[x] = find(parent, parent[x]); - } - parent[x] - } + let mut components = UnionFind::::new(n); for (u, v) in graph.edges() { if config[u] != config[v] { @@ -174,12 +191,9 @@ fn is_valid_forest_partition(graph: &G, num_forests: usize, config: &[ continue; } // Both u and v are in the same class; check for cycle - let ru = find(&mut parent, u); - let rv = find(&mut parent, v); - if ru == rv { + if !components.union(u, v) { return false; // Cycle detected } - parent[ru] = rv; // Union } true @@ -197,13 +211,17 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "partition_into_forests_simplegraph", - instance: Box::new(PartitionIntoForests::new( - SimpleGraph::new( - 6, - vec![(0, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 5), (5, 3)], - ), - 2, - )), + instance: Box::new( + PartitionIntoForests::new( + SimpleGraph::new( + 6, + vec![(0, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 5), (5, 3)], + ) + .unwrap(), + 2, + ) + .unwrap(), + ), // V0={0,3}: edges from graph in class 0: none among {0,3} → forest // V1={1,2,4,5}: edges (1,2),(3,4) but 3∉V1; edges among V1: (1,2),(4,5) → path forest optimal_config: serde_json::json!(vec![0, 1, 1, 0, 1, 1]), 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 5c198c54f..248dbad43 100644 --- a/src/models/graph/partition_into_paths_of_length_2.rs +++ b/src/models/graph/partition_into_paths_of_length_2.rs @@ -51,33 +51,50 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // 6-vertex graph with two P3 paths: 0-1-2 and 3-4-5 -/// let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]); -/// let problem = PartitionIntoPathsOfLength2::new(graph); +/// let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]).unwrap(); +/// let problem = PartitionIntoPathsOfLength2::new(graph).unwrap(); /// /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct PartitionIntoPathsOfLength2 { /// The underlying graph. graph: G, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct PartitionIntoPathsOfLength2Data { + graph: G, +} + +impl<'de, G> Deserialize<'de> for PartitionIntoPathsOfLength2 +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = PartitionIntoPathsOfLength2Data::::deserialize(deserializer)?; + Self::new(data.graph).map_err(serde::de::Error::custom) + } +} + impl PartitionIntoPathsOfLength2 { /// Create a new PartitionIntoPathsOfLength2 problem from a graph. /// - /// # Panics - /// Panics if `graph.num_vertices()` is not divisible by 3. - pub fn new(graph: G) -> Self { - assert_eq!( - graph.num_vertices() % 3, - 0, - "Number of vertices ({}) must be divisible by 3", - graph.num_vertices() - ); - Self { graph } + /// # Errors + /// Returns an error if `graph.num_vertices()` is not divisible by 3. + pub fn new(graph: G) -> Result { + if !graph.num_vertices().is_multiple_of(3) { + return Err(format!( + "Number of vertices ({}) must be divisible by 3", + graph.num_vertices() + ) + .into()); + } + Ok(Self { graph }) } /// Get a reference to the underlying graph. @@ -180,9 +197,12 @@ impl crate::solvers::BruteForceProblem for PartitionIntoPathsOfLength2 where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - let q = self.num_groups(); - vec![q; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_groups()) } } @@ -198,23 +218,29 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "partition_into_paths_of_length_2_simplegraph", - instance: Box::new(PartitionIntoPathsOfLength2::new(SimpleGraph::new( - 9, - vec![ - (0, 1), - (1, 2), - (3, 4), - (4, 5), - (6, 7), - (7, 8), - (0, 3), - (2, 5), - (3, 6), - (5, 8), - (1, 4), - (4, 7), - ], - ))), + instance: Box::new( + PartitionIntoPathsOfLength2::new( + SimpleGraph::new( + 9, + vec![ + (0, 1), + (1, 2), + (3, 4), + (4, 5), + (6, 7), + (7, 8), + (0, 3), + (2, 5), + (3, 6), + (5, 8), + (1, 4), + (4, 7), + ], + ) + .unwrap(), + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![0, 0, 0, 1, 1, 1, 2, 2, 2]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/partition_into_perfect_matchings.rs b/src/models/graph/partition_into_perfect_matchings.rs index e93976e53..1d1034860 100644 --- a/src/models/graph/partition_into_perfect_matchings.rs +++ b/src/models/graph/partition_into_perfect_matchings.rs @@ -48,14 +48,14 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // 4 vertices with edges: (0,1),(2,3),(0,2),(1,3) -/// let graph = SimpleGraph::new(4, vec![(0,1),(2,3),(0,2),(1,3)]); -/// let problem = PartitionIntoPerfectMatchings::new(graph, 2); +/// let graph = SimpleGraph::new(4, vec![(0,1),(2,3),(0,2),(1,3)]).unwrap(); +/// let problem = PartitionIntoPerfectMatchings::new(graph, 2).unwrap(); /// /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct PartitionIntoPerfectMatchings { /// The underlying graph. @@ -64,21 +64,39 @@ pub struct PartitionIntoPerfectMatchings { num_matchings: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct PartitionIntoPerfectMatchingsData { + graph: G, + num_matchings: usize, +} + +impl<'de, G> Deserialize<'de> for PartitionIntoPerfectMatchings +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = PartitionIntoPerfectMatchingsData::::deserialize(deserializer)?; + Self::new(data.graph, data.num_matchings).map_err(serde::de::Error::custom) + } +} + impl PartitionIntoPerfectMatchings { /// Create a new Partition Into Perfect Matchings instance. /// - /// # Panics - /// Panics if `num_matchings` is zero or greater than `graph.num_vertices()`. - pub fn new(graph: G, num_matchings: usize) -> Self { - assert!(num_matchings >= 1, "num_matchings must be at least 1"); - assert!( - num_matchings <= graph.num_vertices(), - "num_matchings must be at most num_vertices" - ); - Self { + /// # Errors + /// Returns an error if `num_matchings` is zero or greater than `graph.num_vertices()`. + pub fn new(graph: G, num_matchings: usize) -> Result { + if num_matchings == 0 { + return Err("num_matchings must be at least 1".into()); + } + if !(num_matchings <= graph.num_vertices()) { + return Err("num_matchings must be at most num_vertices".into()); + } + Ok(Self { graph, num_matchings, - } + }) } /// Get a reference to the underlying graph. @@ -148,8 +166,12 @@ impl crate::solvers::BruteForceProblem for PartitionIntoPerfectMatchings where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.num_matchings; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_matchings) } } @@ -208,10 +230,13 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "partition_into_perfect_matchings_simplegraph", - instance: Box::new(PartitionIntoPerfectMatchings::new( - SimpleGraph::new(4, vec![(0, 1), (2, 3), (0, 2), (1, 3)]), - 2, - )), + instance: Box::new( + PartitionIntoPerfectMatchings::new( + SimpleGraph::new(4, vec![(0, 1), (2, 3), (0, 2), (1, 3)]).unwrap(), + 2, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![0, 0, 1, 1]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/partition_into_triangles.rs b/src/models/graph/partition_into_triangles.rs index 8638705d2..86c760e95 100644 --- a/src/models/graph/partition_into_triangles.rs +++ b/src/models/graph/partition_into_triangles.rs @@ -43,32 +43,50 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Triangle graph: 3 vertices forming a single triangle -/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); -/// let problem = PartitionIntoTriangles::new(graph); +/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); +/// let problem = PartitionIntoTriangles::new(graph).unwrap(); /// /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct PartitionIntoTriangles { /// The underlying graph. graph: G, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct PartitionIntoTrianglesData { + graph: G, +} + +impl<'de, G> Deserialize<'de> for PartitionIntoTriangles +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = PartitionIntoTrianglesData::::deserialize(deserializer)?; + Self::new(data.graph).map_err(serde::de::Error::custom) + } +} + impl PartitionIntoTriangles { /// Create a new Partition Into Triangles problem from a graph. /// - /// # Panics - /// Panics if the number of vertices is not divisible by 3. - pub fn new(graph: G) -> Self { - assert!( - graph.num_vertices().is_multiple_of(3), - "Number of vertices ({}) must be divisible by 3", - graph.num_vertices() - ); - Self { graph } + /// # Errors + /// Returns an error if the number of vertices is not divisible by 3. + pub fn new(graph: G) -> Result { + if !(graph.num_vertices().is_multiple_of(3)) { + return Err(format!( + "Number of vertices ({}) must be divisible by 3", + graph.num_vertices() + ) + .into()); + } + Ok(Self { graph }) } /// Get a reference to the underlying graph. @@ -168,9 +186,12 @@ impl crate::solvers::BruteForceProblem for PartitionIntoTriangles where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - let q = self.graph.num_vertices() / 3; - vec![q; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices() / 3) } } @@ -186,10 +207,16 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "partition_into_triangles_simplegraph", - instance: Box::new(PartitionIntoTriangles::new(SimpleGraph::new( - 6, - vec![(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (0, 3)], - ))), + instance: Box::new( + PartitionIntoTriangles::new( + SimpleGraph::new( + 6, + vec![(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (0, 3)], + ) + .unwrap(), + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![0, 0, 0, 1, 1, 1]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/path_constrained_network_flow.rs b/src/models/graph/path_constrained_network_flow.rs index e9f324664..1f3452d4c 100644 --- a/src/models/graph/path_constrained_network_flow.rs +++ b/src/models/graph/path_constrained_network_flow.rs @@ -83,13 +83,8 @@ impl TryFrom for PathConstrainedNetworkFlo .transpose()? .unwrap_or(0); let num_vertices = spec.num_vertices.unwrap_or(inferred); - if num_vertices < inferred { - return Err(format!( - "num_vertices {num_vertices} is too small for arc endpoints; need at least {inferred}" - ).into()); - } let capacities = spec.capacities.unwrap_or_else(|| vec![1; spec.arcs.len()]); - let graph = DirectedGraph::new(num_vertices, spec.arcs); + let graph = DirectedGraph::new(num_vertices, spec.arcs)?; Self::try_new( graph, capacities, @@ -327,11 +322,14 @@ impl Problem for PathConstrainedNetworkFlow { } impl crate::solvers::BruteForceProblem for PathConstrainedNetworkFlow { - fn dimensions(&self) -> Vec { - self.paths - .iter() - .map(|path| (self.path_bottleneck(path) as usize) + 1) - .collect() + fn num_variables(&self) -> Result { + Ok(self.paths.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from( + i128::from(self.path_bottleneck(&self.paths[variable])) + 1, + )?) } } @@ -362,7 +360,8 @@ pub(crate) fn canonical_model_example_specs() -> Vec::new(graph, vec![5, 2, 5], vec![1, 6], 1, 2).unwrap(); /// // V_F = {0,1,2}, E_F = {(0,1)} gives two components {0,1} and {2}: @@ -198,7 +198,7 @@ fn simple_graph_from_create( "num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}" ))); } - Ok(SimpleGraph::new(num_vertices, edges)) + SimpleGraph::new(num_vertices, edges) } impl PrizeCollectingSteinerForest { @@ -223,12 +223,27 @@ impl PrizeCollectingSteinerForest { } for (index, prize) in vertex_prizes.iter().enumerate() { prize.validate_element(&format!("vertex prize at index {index}"))?; + if prize.to_sum() < W::Sum::zero() { + return Err(ConstructionError::InvalidInput(format!( + "vertex prize at index {index} must be nonnegative" + ))); + } } for (index, cost) in edge_costs.iter().enumerate() { cost.validate_element(&format!("edge cost at index {index}"))?; + if cost.to_sum() < W::Sum::zero() { + return Err(ConstructionError::InvalidInput(format!( + "edge cost at index {index} must be nonnegative" + ))); + } } beta.validate_element("beta")?; omega.validate_element("omega")?; + if beta.to_sum() < W::Sum::zero() || omega.to_sum() < W::Sum::zero() { + return Err(ConstructionError::InvalidInput( + "beta and omega must be nonnegative".into(), + )); + } Ok(Self { graph, vertex_prizes, @@ -389,8 +404,16 @@ where G: Graph + VariantParam, W: WeightElement + VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices() + self.graph.num_edges()] + fn num_variables(&self) -> Result { + (self.graph.num_vertices()) + .checked_add(self.graph.num_edges()) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -468,7 +491,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![5, 2, 5], vec![1, 6], 1, diff --git a/src/models/graph/rooted_tree_arrangement.rs b/src/models/graph/rooted_tree_arrangement.rs index 316e28187..e0a9e5e39 100644 --- a/src/models/graph/rooted_tree_arrangement.rs +++ b/src/models/graph/rooted_tree_arrangement.rs @@ -160,9 +160,16 @@ impl crate::solvers::BruteForceProblem for RootedTreeArrangement where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - vec![n; 2 * n] + fn num_variables(&self) -> Result { + (2usize) + .checked_mul(self.graph.num_vertices()) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } @@ -301,7 +308,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec { /// The underlying graph. graph: G, @@ -62,6 +62,26 @@ pub struct RuralPostman { required_edges: Vec, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))] +struct RuralPostmanData { + graph: G, + edge_lengths: Vec, + required_edges: Vec, +} + +impl<'de, G, W> Deserialize<'de> for RuralPostman +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = RuralPostmanData::::deserialize(deserializer)?; + Self::new(data.graph, data.edge_lengths, data.required_edges) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct RuralPostmanCreateSpec { #[create(codec = "edge-list")] @@ -81,22 +101,7 @@ impl TryFrom for RuralPostman { let edge_lengths = spec .edge_weights .unwrap_or_else(|| vec![1; graph.num_edges()]); - if edge_lengths.len() != graph.num_edges() { - return Err(format!( - "edge_weights has length {}, expected {}", - edge_lengths.len(), - graph.num_edges() - ) - .into()); - } - if let Some(&edge) = spec - .required_edges - .iter() - .find(|&&edge| edge >= graph.num_edges()) - { - return Err(format!("required edge index {edge} is out of bounds").into()); - } - Ok(Self::new(graph, edge_lengths, spec.required_edges)) + Self::new(graph, edge_lengths, spec.required_edges) } } @@ -128,34 +133,36 @@ fn simple_graph_from_create( ) .into()); } - Ok(SimpleGraph::new(num_vertices, edges)) + SimpleGraph::new(num_vertices, edges) } impl RuralPostman { /// Create a new RuralPostman problem. /// - /// # Panics - /// Panics if edge_lengths length does not match graph edges, + /// # Errors + /// Returns an error if edge_lengths length does not match graph edges, /// or if any required edge index is out of bounds. - pub fn new(graph: G, edge_lengths: Vec, required_edges: Vec) -> Self { - assert_eq!( - edge_lengths.len(), - graph.num_edges(), - "edge_lengths length must match num_edges" - ); + pub fn new( + graph: G, + edge_lengths: Vec, + required_edges: Vec, + ) -> Result { + Self::check_weights(&graph, &edge_lengths)?; for &idx in &required_edges { - assert!( - idx < graph.num_edges(), - "required edge index {} out of bounds (graph has {} edges)", - idx, - graph.num_edges() - ); + if !(idx < graph.num_edges()) { + return Err(format!( + "required edge index {} out of bounds (graph has {} edges)", + idx, + graph.num_edges() + ) + .into()); + } } - Self { + Ok(Self { graph, edge_lengths, required_edges, - } + }) } /// Get a reference to the underlying graph. @@ -189,9 +196,20 @@ impl RuralPostman { } /// Set new edge lengths. - pub fn set_weights(&mut self, weights: Vec) { - assert_eq!(weights.len(), self.graph.num_edges()); + pub fn set_weights( + &mut self, + weights: Vec, + ) -> Result<(), crate::registry::ConstructionError> { + Self::check_weights(&self.graph, &weights)?; self.edge_lengths = weights; + Ok(()) + } + + fn check_weights(graph: &G, weights: &[W]) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_edges() { + return Err("edge_lengths length must match num_edges".into()); + } + Ok(()) } /// Get the edge lengths as a Vec. @@ -354,8 +372,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![3; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(3usize) } } @@ -384,14 +406,13 @@ pub(crate) fn canonical_model_example_specs() -> Vec { /// The underlying graph. graph: G, @@ -67,6 +67,39 @@ pub struct ShortestWeightConstrainedPath { weight_bound: N::Sum, } +#[derive(Deserialize)] +#[serde(bound( + deserialize = "G: Graph + Deserialize<'de>, N: WeightElement + Deserialize<'de>, N::Sum: Deserialize<'de>" +))] +struct ShortestWeightConstrainedPathData { + graph: G, + edge_lengths: Vec, + edge_weights: Vec, + source_vertex: usize, + target_vertex: usize, + weight_bound: N::Sum, +} + +impl<'de, G, N> Deserialize<'de> for ShortestWeightConstrainedPath +where + G: Graph + Deserialize<'de>, + N: WeightElement + Deserialize<'de>, + N::Sum: Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = ShortestWeightConstrainedPathData::::deserialize(deserializer)?; + Self::new( + data.graph, + data.edge_lengths, + data.edge_weights, + data.source_vertex, + data.target_vertex, + data.weight_bound, + ) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct ShortestWeightConstrainedPathCreateSpec { /// The underlying graph G=(V,E). @@ -88,75 +121,37 @@ impl TryFrom { type Error = crate::registry::ConstructionError; fn try_from(spec: ShortestWeightConstrainedPathCreateSpec) -> Result { - let edge_count = spec.graph.num_edges(); - if spec.edge_lengths.len() != edge_count { - return Err(format!( - "edge_lengths has {} entries, expected {edge_count}", - spec.edge_lengths.len() - ) - .into()); - } - if spec.edge_weights.len() != edge_count { - return Err(format!( - "edge_weights has {} entries, expected {edge_count}", - spec.edge_weights.len() - ) - .into()); - } - if spec.edge_lengths.iter().any(|&value| value <= 0) { - return Err("edge_lengths must be positive".to_string().into()); - } - if spec.edge_weights.iter().any(|&value| value <= 0) { - return Err("edge_weights must be positive".to_string().into()); - } - let vertex_count = spec.graph.num_vertices(); - if spec.source_vertex >= vertex_count { - return Err(format!( - "source_vertex {} is outside graph with {vertex_count} vertices", - spec.source_vertex - ) - .into()); - } - if spec.target_vertex >= vertex_count { - return Err(format!( - "target_vertex {} is outside graph with {vertex_count} vertices", - spec.target_vertex - ) - .into()); - } - if spec.weight_bound <= 0 { - return Err("weight_bound must be positive".to_string().into()); - } - Ok(Self::new( + Self::new( spec.graph, spec.edge_lengths, spec.edge_weights, spec.source_vertex, spec.target_vertex, spec.weight_bound, - )) + ) } } impl ShortestWeightConstrainedPath { - fn assert_positive_edge_values(values: &[N], label: &str) { - let zero = N::Sum::zero(); - assert!( - values.iter().all(|value| value.to_sum() > zero.clone()), - "All {label} must be positive (> 0)" - ); - } - - fn assert_positive_bound(bound: &N::Sum, label: &str) { - let zero = N::Sum::zero(); - assert!(bound > &zero, "{label} must be positive (> 0)"); + fn check_edge_values( + graph: &G, + values: &[N], + label: &str, + ) -> Result<(), crate::registry::ConstructionError> { + if values.len() != graph.num_edges() { + return Err(format!("{label} length must match num_edges").into()); + } + if !values.iter().all(|value| value.to_sum() > N::Sum::zero()) { + return Err(format!("all {label} must be positive (> 0)").into()); + } + Ok(()) } /// Create a new ShortestWeightConstrainedPath instance. /// - /// # Panics + /// # Errors /// - /// Panics if either edge vector length does not match the graph's edge + /// Returns an error if either edge vector length does not match the graph's edge /// count, or if the source / target vertices are out of bounds. pub fn new( graph: G, @@ -165,40 +160,36 @@ impl ShortestWeightConstrainedPath { source_vertex: usize, target_vertex: usize, weight_bound: N::Sum, - ) -> Self { - assert_eq!( - edge_lengths.len(), - graph.num_edges(), - "edge_lengths length must match num_edges" - ); - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - Self::assert_positive_edge_values(&edge_lengths, "edge lengths"); - Self::assert_positive_edge_values(&edge_weights, "edge weights"); - assert!( - source_vertex < graph.num_vertices(), - "source_vertex {} out of bounds (graph has {} vertices)", - source_vertex, - graph.num_vertices() - ); - assert!( - target_vertex < graph.num_vertices(), - "target_vertex {} out of bounds (graph has {} vertices)", - target_vertex, - graph.num_vertices() - ); - Self::assert_positive_bound(&weight_bound, "weight_bound"); - Self { + ) -> Result { + Self::check_edge_values(&graph, &edge_lengths, "edge lengths")?; + Self::check_edge_values(&graph, &edge_weights, "edge weights")?; + if !(source_vertex < graph.num_vertices()) { + return Err(format!( + "source_vertex {} out of bounds (graph has {} vertices)", + source_vertex, + graph.num_vertices() + ) + .into()); + } + if !(target_vertex < graph.num_vertices()) { + return Err(format!( + "target_vertex {} out of bounds (graph has {} vertices)", + target_vertex, + graph.num_vertices() + ) + .into()); + } + if weight_bound.partial_cmp(&N::Sum::zero()) != Some(std::cmp::Ordering::Greater) { + return Err("weight_bound must be positive (> 0)".into()); + } + Ok(Self { graph, edge_lengths, edge_weights, source_vertex, target_vertex, weight_bound, - } + }) } /// Get a reference to the underlying graph. @@ -217,25 +208,23 @@ impl ShortestWeightConstrainedPath { } /// Set new edge lengths. - pub fn set_lengths(&mut self, edge_lengths: Vec) { - assert_eq!( - edge_lengths.len(), - self.graph.num_edges(), - "edge_lengths length must match num_edges" - ); - Self::assert_positive_edge_values(&edge_lengths, "edge lengths"); + pub fn set_lengths( + &mut self, + edge_lengths: Vec, + ) -> Result<(), crate::registry::ConstructionError> { + Self::check_edge_values(&self.graph, &edge_lengths, "edge_lengths")?; self.edge_lengths = edge_lengths; + Ok(()) } /// Set new edge weights. - pub fn set_weights(&mut self, edge_weights: Vec) { - assert_eq!( - edge_weights.len(), - self.graph.num_edges(), - "edge_weights length must match num_edges" - ); - Self::assert_positive_edge_values(&edge_weights, "edge weights"); + pub fn set_weights( + &mut self, + edge_weights: Vec, + ) -> Result<(), crate::registry::ConstructionError> { + Self::check_edge_values(&self.graph, &edge_weights, "edge_weights")?; self.edge_weights = edge_weights; + Ok(()) } /// Get the source vertex. @@ -356,8 +345,12 @@ where G: Graph + crate::variant::VariantParam, N: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -365,26 +358,30 @@ where pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "shortest_weight_constrained_path_simplegraph", - instance: Box::new(ShortestWeightConstrainedPath::new( - SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (1, 3), - (2, 3), - (2, 4), - (3, 5), - (4, 5), - (1, 4), - ], - ), - vec![2, 4, 3, 1, 5, 4, 2, 6], - vec![5, 1, 2, 3, 2, 3, 1, 1], - 0, - 5, - 8, - )), + instance: Box::new( + ShortestWeightConstrainedPath::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 3), + (2, 3), + (2, 4), + (3, 5), + (4, 5), + (1, 4), + ], + ) + .unwrap(), + vec![2, 4, 3, 1, 5, 4, 2, 6], + vec![5, 1, 2, 3, 2, 3, 1, 1], + 0, + 5, + 8, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![ false, true, false, true, false, true, false, false ]), diff --git a/src/models/graph/spin_glass.rs b/src/models/graph/spin_glass.rs index b99974051..83e63abe5 100644 --- a/src/models/graph/spin_glass.rs +++ b/src/models/graph/spin_glass.rs @@ -140,17 +140,12 @@ macro_rules! spin_glass_create_spec { .transpose()? .unwrap_or(0); let num_vertices = spec.num_vertices.unwrap_or(inferred); - if num_vertices < inferred { - return Err(ConstructionError::Conversion(format!( - "num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}" - ))); - } let couplings = spec .couplings .unwrap_or_else(|| vec![$one; spec.graph.len()]); let fields = spec.fields.unwrap_or_else(|| vec![$zero; num_vertices]); SpinGlass::from_graph( - SimpleGraph::new(num_vertices, spec.graph), + SimpleGraph::new(num_vertices, spec.graph)?, couplings, fields, ) @@ -186,7 +181,7 @@ impl SpinGlass { .iter() .map(|(_, coupling)| coupling.clone()) .collect(); - let graph = SimpleGraph::new(num_spins, edges); + let graph = SimpleGraph::new(num_spins, edges)?; Self::from_graph(graph, couplings, fields) } @@ -392,8 +387,12 @@ where + num_traits::Zero + num_traits::Bounded, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/steiner_tree.rs b/src/models/graph/steiner_tree.rs index 6f819ecae..08efb3312 100644 --- a/src/models/graph/steiner_tree.rs +++ b/src/models/graph/steiner_tree.rs @@ -47,6 +47,10 @@ inventory::submit! { /// - Selected edges form a tree (connected + acyclic) /// - All terminal vertices are included /// +/// At least one terminal is required. With one terminal, selecting no edges +/// represents the tree consisting of that terminal alone. Signed edge weights +/// are allowed; additional edges must still form a tree containing the terminal. +/// /// # Type Parameters /// /// * `G` - The graph type (e.g., `SimpleGraph`) @@ -105,8 +109,8 @@ impl SteinerTree { if edge_weights.len() != graph.num_edges() { return Err("edge_weights length must match num_edges".into()); } - if terminals.len() < 2 { - return Err("at least 2 terminals required".into()); + if terminals.is_empty() { + return Err("at least one terminal required".into()); } let distinct_terminals: BTreeSet<_> = terminals.iter().copied().collect(); if distinct_terminals.len() != terminals.len() { @@ -222,7 +226,7 @@ fn is_valid_steiner_tree(graph: &G, terminals: &[usize], config: &[boo } if selected_count == 0 { - return false; + return terminals.len() == 1; } // BFS from first terminal to check connectivity @@ -290,13 +294,11 @@ where let mut total = W::Sum::zero(); for (idx, &selected) in config.iter().enumerate() { if selected { - if let Some(w) = self.edge_weights.get(idx) { - total = W::checked_add_to_sum( - total, - w.to_sum(), - "summing Steiner tree edge weights", - )?; - } + total = W::checked_add_to_sum( + total, + self.edge_weights[idx].to_sum(), + "summing Steiner tree edge weights", + )?; } } Min(Some(total)) @@ -309,8 +311,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -345,8 +351,10 @@ impl TryFrom for SteinerTree { } } +// For signed weights, enumerate nonterminal vertex subsets and compute an MST +// on each induced graph. Terminal-subset shortest-path DP assumes nonnegative weights. crate::declare_variants! { - default SteinerTree => "3^num_terminals * num_vertices + 2^num_terminals * num_vertices^2" create SteinerTreeCreateSpec random, + default SteinerTree => "2^num_vertices * 0.5^num_terminals * num_vertices^2" create SteinerTreeCreateSpec random, SteinerTree => "3^num_terminals * num_vertices + 2^num_terminals * num_vertices^2" create SteinerTreeOneCreateSpec, } @@ -363,7 +371,8 @@ pub(crate) fn canonical_model_example_specs() -> Vec::FIELDS, - } -} - -/// The Steiner Tree in Graphs problem. -/// -/// Given a weighted graph G = (V, E) with edge weights w_e and a -/// subset R ⊆ V of required terminal vertices, find a subtree T of G -/// that includes all vertices of R and minimizes the total edge weight -/// Σ_{e ∈ T} w(e). -/// -/// # Representation -/// -/// Each edge is assigned a binary variable: -/// - 0: edge is not in the tree -/// - 1: edge is in the tree -/// -/// A valid Steiner tree requires: -/// - All terminal vertices are connected through selected edges -/// - Selected edges form a connected subgraph (optimally a tree) -/// -/// # Type Parameters -/// -/// * `G` - The graph type (e.g., `SimpleGraph`) -/// * `W` - The weight type for edges (e.g., `i64`, `f64`) -/// -/// # Example -/// -/// ``` -/// use problemreductions::models::graph::SteinerTreeInGraphs; -/// use problemreductions::topology::SimpleGraph; -/// use problemreductions::{Problem, BruteForce}; -/// -/// // Path graph 0-1-2-3, terminals {0, 3} -/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); -/// let problem = SteinerTreeInGraphs::new(graph, vec![0, 3], vec![1, 1, 1]); -/// -/// let solver = BruteForce::new(); -/// let solution = solver.solve(&problem).unwrap().unwrap(); -/// // Optimal: select all 3 edges (the only path from 0 to 3) -/// assert_eq!(solution, vec![true, true, true]); -/// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SteinerTreeInGraphs { - /// The underlying graph. - graph: G, - /// Required terminal vertices. - terminals: Vec, - /// Weights for each edge (in edge index order). - edge_weights: Vec, -} - -#[derive(Debug, Deserialize, crate::CreateSpec)] -struct SteinerTreeInGraphsCreateSpec { - /// The underlying graph. - graph: SimpleGraph, - /// Required terminal vertices. - terminals: Vec, - /// Edge weights; defaults to one per edge. - edge_weights: Option>, -} -impl TryFrom> for SteinerTreeInGraphs -where - W: WeightElement, -{ - type Error = crate::registry::ConstructionError; - fn try_from(spec: SteinerTreeInGraphsCreateSpec) -> Result { - let count = spec.graph.num_edges(); - let edge_weights = spec - .edge_weights - .unwrap_or_else(|| (0..count).map(|_| W::unit()).collect()); - if edge_weights.len() != count { - return Err(format!( - "edge_weights has {} entries, expected {count}", - edge_weights.len() - ) - .into()); - } - if let Some(&terminal) = spec - .terminals - .iter() - .find(|&&t| t >= spec.graph.num_vertices()) - { - return Err(format!("terminal {terminal} is outside the graph").into()); - } - Ok(Self::new(spec.graph, spec.terminals, edge_weights)) - } -} - -impl SteinerTreeInGraphs { - /// Create a SteinerTreeInGraphs problem from a graph, terminals, and edge weights. - /// - /// # Panics - /// Panics if `edge_weights.len() != graph.num_edges()` or any terminal index is out of bounds. - pub fn new(graph: G, terminals: Vec, edge_weights: Vec) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - for &t in &terminals { - assert!( - t < graph.num_vertices(), - "terminal vertex {} out of bounds (num_vertices = {})", - t, - graph.num_vertices() - ); - } - Self { - graph, - terminals, - edge_weights, - } - } - - /// Get a reference to the underlying graph. - pub fn graph(&self) -> &G { - &self.graph - } - - /// Get the terminal vertices. - pub fn terminals(&self) -> &[usize] { - &self.terminals - } - - /// Get all edges with their weights. - pub fn edges(&self) -> Vec<(usize, usize, W)> { - self.graph - .edges() - .into_iter() - .zip(self.edge_weights.iter().cloned()) - .map(|((u, v), w)| (u, v, w)) - .collect() - } - - /// Set new weights for the problem. - pub fn set_weights(&mut self, weights: Vec) { - assert_eq!(weights.len(), self.graph.num_edges()); - self.edge_weights = weights; - } - - /// Get the weights for the problem. - pub fn weights(&self) -> Vec { - self.edge_weights.clone() - } - - /// Check if the problem uses a non-unit weight type. - pub fn is_weighted(&self) -> bool - where - W: WeightElement, - { - !W::IS_UNIT - } - - /// Check if a configuration is a valid Steiner tree. - pub fn is_valid_solution(&self, config: &[usize]) -> bool { - if config.len() != self.graph.num_edges() { - return false; - } - let selected: Vec = config.iter().map(|&s| s == 1).collect(); - is_steiner_tree(&self.graph, &self.terminals, &selected) - } -} - -impl SteinerTreeInGraphs { - /// Get the number of vertices in the underlying graph. - pub fn num_vertices(&self) -> usize { - self.graph().num_vertices() - } - - /// Get the number of edges in the underlying graph. - pub fn num_edges(&self) -> usize { - self.graph().num_edges() - } - - /// Get the number of terminal vertices. - pub fn num_terminals(&self) -> usize { - self.terminals.len() - } -} - -impl Problem for SteinerTreeInGraphs -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - const NAME: &'static str = "SteinerTreeInGraphs"; - type Solution = Vec; - type Value = Min; - - crate::problem_parameters![ - ("num_edges", num_edges), - ("num_terminals", num_terminals), - ("num_vertices", num_vertices), - ]; - - fn variant() -> Vec<(&'static str, &'static str)> { - crate::variant_params![G, W] - } - - fn evaluate( - &self, - config: &Self::Solution, - ) -> Result, crate::traits::EvaluationError> { - Ok({ - if config.len() != self.graph.num_edges() { - return Err(crate::traits::EvaluationError::InvalidConfiguration( - "edge-selection length does not match the graph".into(), - )); - } - let selected = config; - if !is_steiner_tree(&self.graph, &self.terminals, selected) { - return Ok(Min(None)); - } - let mut total = W::Sum::zero(); - for (idx, &sel) in config.iter().enumerate() { - if sel { - if let Some(w) = self.edge_weights.get(idx) { - total = W::checked_add_to_sum( - total, - w.to_sum(), - "summing Steiner tree edge weights", - )?; - } - } - } - Min(Some(total)) - }) - } -} - -impl crate::solvers::BruteForceProblem for SteinerTreeInGraphs -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] - } -} - -/// Check if a selection of edges forms a valid Steiner tree (connected subgraph spanning all terminals). -/// -/// A valid Steiner tree requires: -/// 1. All terminal vertices are reachable from each other through selected edges. -/// 2. The selected edges form a connected subgraph that includes all terminals. -/// -/// Note: The optimal solution is always a tree, but we accept any connected subgraph -/// spanning all terminals (the brute-force solver will find the minimum-weight one). -/// -/// # Panics -/// Panics if `selected.len() != graph.num_edges()`. -pub(crate) fn is_steiner_tree(graph: &G, terminals: &[usize], selected: &[bool]) -> bool { - assert_eq!( - selected.len(), - graph.num_edges(), - "selected length must match num_edges" - ); - - // If no terminals, any selection is trivially valid (including empty) - if terminals.is_empty() { - return true; - } - - // If only one terminal, it's valid as long as that terminal exists - // (no edges needed to connect a single vertex) - if terminals.len() == 1 { - return true; - } - - // Build adjacency list from selected edges - let n = graph.num_vertices(); - let edges = graph.edges(); - let mut adj: Vec> = vec![vec![]; n]; - - let mut has_any_edge = false; - for (idx, &sel) in selected.iter().enumerate() { - if sel { - let (u, v) = edges[idx]; - adj[u].push(v); - adj[v].push(u); - has_any_edge = true; - } - } - - if !has_any_edge { - return false; - } - - // BFS from the first terminal to check connectivity of all terminals - let start = terminals[0]; - let mut visited = vec![false; n]; - let mut queue = std::collections::VecDeque::new(); - visited[start] = true; - queue.push_back(start); - - while let Some(node) = queue.pop_front() { - for &neighbor in &adj[node] { - if !visited[neighbor] { - visited[neighbor] = true; - queue.push_back(neighbor); - } - } - } - - // All terminals must be reachable - terminals.iter().all(|&t| visited[t]) -} - -crate::impl_random_generate!(SteinerTreeInGraphs, crate::random::SimpleGraphRandomSpec, |spec| { - if spec.num_vertices < 2 { - return Err("num_vertices must be at least 2".to_string().into()); - } - let graph = spec.graph()?; - let terminals = (0..std::cmp::max(2, spec.num_vertices / 2)).collect(); - let weights = vec![1; graph.num_edges()]; - Ok(SteinerTreeInGraphs::new(graph, terminals, weights)) -}); - -#[derive(Debug, Deserialize, crate::CreateSpec)] -struct SteinerTreeInGraphsOneCreateSpec { - /// The underlying graph. - graph: SimpleGraph, - terminals: Vec, -} - -impl TryFrom for SteinerTreeInGraphs { - type Error = crate::registry::ConstructionError; - fn try_from(spec: SteinerTreeInGraphsOneCreateSpec) -> Result { - let weights = vec![One; spec.graph.num_edges()]; - if let Some(&terminal) = spec - .terminals - .iter() - .find(|&&t| t >= spec.graph.num_vertices()) - { - return Err(format!("terminal {terminal} is outside the graph").into()); - } - Ok(Self::new(spec.graph, spec.terminals, weights)) - } -} - -crate::declare_variants! { - default SteinerTreeInGraphs => "2^num_terminals * num_vertices^3" create SteinerTreeInGraphsCreateSpec random, - SteinerTreeInGraphs => "2^num_terminals * num_vertices^3" create SteinerTreeInGraphsOneCreateSpec, -} - -crate::register_brute_force! { - SteinerTreeInGraphs decode |_, indices: Vec| crate::config::config_to_bits(&indices), - SteinerTreeInGraphs decode |_, indices: Vec| crate::config::config_to_bits(&indices), -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_model_example_specs() -> Vec { - vec![crate::example_db::specs::ModelExampleSpec { - id: "steiner_tree_in_graphs_simplegraph", - instance: Box::new(SteinerTreeInGraphs::new( - SimpleGraph::new( - 6, - vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 5), (3, 4), (4, 5)], - ), - vec![0, 3, 5], - vec![3, 2, 4, 1, 2, 3, 1], - )), - // Optimal: edges {0,2}(w=2), {2,3}(w=1), {2,5}(w=2) = weight 5 - optimal_config: serde_json::json!(vec![false, true, false, true, true, false, false]), - optimal_value: serde_json::json!(5), - }] -} - -#[cfg(test)] -#[path = "../../unit_tests/models/graph/steiner_tree_in_graphs.rs"] -mod tests; diff --git a/src/models/graph/strong_connectivity_augmentation.rs b/src/models/graph/strong_connectivity_augmentation.rs index 83c2c6cdf..2963f395d 100644 --- a/src/models/graph/strong_connectivity_augmentation.rs +++ b/src/models/graph/strong_connectivity_augmentation.rs @@ -174,7 +174,11 @@ impl StrongConnectivityAugmentation { } } - Ok(DirectedGraph::new(self.graph.num_vertices(), augmented_arcs).is_strongly_connected()) + Ok( + DirectedGraph::new(self.graph.num_vertices(), augmented_arcs) + .expect("candidate arc endpoints were checked at construction") + .is_strongly_connected(), + ) } } @@ -213,8 +217,12 @@ impl crate::solvers::BruteForceProblem for StrongConnectivityAugmentation where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.candidate_arcs.len()] + fn num_variables(&self) -> Result { + Ok(self.candidate_arcs.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -255,7 +263,7 @@ pub(crate) fn canonical_model_example_specs() -> Vechost 0, 1->1, 2->2 @@ -175,16 +175,18 @@ impl Problem for SubgraphIsomorphism { } impl crate::solvers::BruteForceProblem for SubgraphIsomorphism { - fn dimensions(&self) -> Vec { - let n_host = self.host_graph.num_vertices(); - let n_pattern = self.pattern_graph.num_vertices(); + fn num_variables(&self) -> Result { + Ok(self.pattern_graph.num_vertices()) + } - if n_pattern > n_host { - // No injective mapping possible: each variable gets an empty domain. - vec![0; n_pattern] - } else { - vec![n_host; n_pattern] - } + fn dimension(&self, _variable: usize) -> Result { + Ok( + if self.pattern_graph.num_vertices() > self.host_graph.num_vertices() { + 0 + } else { + self.host_graph.num_vertices() + }, + ) } } @@ -203,8 +205,8 @@ pub(crate) fn canonical_model_example_specs() -> Vec { /// The underlying graph. graph: G, @@ -55,6 +55,23 @@ pub struct TravelingSalesman { edge_weights: Vec, } +#[derive(Deserialize)] +struct TravelingSalesmanData { + graph: G, + edge_weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for TravelingSalesman +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = TravelingSalesmanData::deserialize(deserializer)?; + Self::new(data.graph, data.edge_weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct TravelingSalesmanCreateSpec { #[create(codec = "edge-list")] @@ -72,15 +89,7 @@ impl TryFrom for TravelingSalesman TravelingSalesman { /// Create a TravelingSalesman problem from a graph with given edge weights. - pub fn new(graph: G, edge_weights: Vec) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - Self { + pub fn new(graph: G, edge_weights: Vec) -> Result { + Self::check_weights(&graph, &edge_weights)?; + Ok(Self { graph, edge_weights, - } + }) } /// Create a TravelingSalesman problem with unit weights. @@ -157,9 +162,23 @@ impl TravelingSalesman { } /// Set new weights for the problem. - pub fn set_weights(&mut self, weights: Vec) { - assert_eq!(weights.len(), self.graph.num_edges()); + pub fn set_weights( + &mut self, + weights: Vec, + ) -> Result<(), crate::registry::ConstructionError> { + Self::check_weights(&self.graph, &weights)?; self.edge_weights = weights; + Ok(()) + } + + fn check_weights( + graph: &G, + edge_weights: &[W], + ) -> Result<(), crate::registry::ConstructionError> { + if edge_weights.len() != graph.num_edges() { + return Err("edge_weights length must match graph num_edges".into()); + } + Ok(()) } /// Get the weights for the problem. @@ -252,8 +271,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -331,10 +354,13 @@ pub(crate) fn is_hamiltonian_cycle(graph: &G, selected: &[bool]) -> bo pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "traveling_salesman_simplegraph", - instance: Box::new(TravelingSalesman::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), - vec![1, 3, 2, 2, 3, 1], - )), + instance: Box::new( + TravelingSalesman::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), + vec![1, 3, 2, 2, 3, 1], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![true, false, true, true, false, true]), optimal_value: serde_json::json!(6), }] @@ -343,7 +369,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec, crate::random::SimpleGraphRandomSpec, |spec| { let graph = spec.graph()?; let weights = vec![1; graph.num_edges()]; - Ok(TravelingSalesman::new(graph, weights)) + Ok(TravelingSalesman::new(graph, weights).unwrap()) }); crate::declare_variants! { diff --git a/src/models/graph/undirected_flow_lower_bounds.rs b/src/models/graph/undirected_flow_lower_bounds.rs index ff9b0657f..1db23c73f 100644 --- a/src/models/graph/undirected_flow_lower_bounds.rs +++ b/src/models/graph/undirected_flow_lower_bounds.rs @@ -33,6 +33,7 @@ inventory::submit! { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "UndirectedFlowLowerBoundsCreateSpec")] pub struct UndirectedFlowLowerBounds { graph: SimpleGraph, capacities: Vec, @@ -60,50 +61,14 @@ struct UndirectedFlowLowerBoundsCreateSpec { impl TryFrom for UndirectedFlowLowerBounds { type Error = crate::registry::ConstructionError; fn try_from(spec: UndirectedFlowLowerBoundsCreateSpec) -> Result { - let edges = spec.graph.num_edges(); - if spec.capacities.len() != edges { - return Err(format!( - "capacities has {} entries, expected {edges}", - spec.capacities.len() - ) - .into()); - } - if spec.lower_bounds.len() != edges { - return Err(format!( - "lower_bounds has {} entries, expected {edges}", - spec.lower_bounds.len() - ) - .into()); - } - let vertices = spec.graph.num_vertices(); - if spec.source >= vertices || spec.sink >= vertices { - return Err("source and sink must be valid graph vertices" - .to_string() - .into()); - } - if spec.source == spec.sink { - return Err("source and sink must be distinct".to_string().into()); - } - if spec.requirement == 0 { - return Err("requirement must be at least 1".to_string().into()); - } - if let Some((index, _)) = spec - .lower_bounds - .iter() - .zip(&spec.capacities) - .enumerate() - .find(|(_, (&lower, &upper))| lower > upper) - { - return Err(format!("lower bound at edge {index} exceeds its capacity").into()); - } - Ok(Self::new( + Self::new( spec.graph, spec.capacities, spec.lower_bounds, spec.source, spec.sink, spec.requirement, - )) + ) } } @@ -115,45 +80,45 @@ impl UndirectedFlowLowerBounds { source: usize, sink: usize, requirement: i64, - ) -> Self { - assert_eq!( - capacities.len(), - graph.num_edges(), - "capacities length must match graph num_edges" - ); - assert_eq!( - lower_bounds.len(), - graph.num_edges(), - "lower_bounds length must match graph num_edges" - ); + ) -> Result { + if capacities.len() != graph.num_edges() { + return Err("capacities length must match graph num_edges".into()); + } + if lower_bounds.len() != graph.num_edges() { + return Err("lower_bounds length must match graph num_edges".into()); + } let num_vertices = graph.num_vertices(); - assert!( - source < num_vertices, - "source must be less than num_vertices ({num_vertices})" - ); - assert!( - sink < num_vertices, - "sink must be less than num_vertices ({num_vertices})" - ); - assert!(source != sink, "source and sink must be distinct"); - assert!(requirement >= 1, "requirement must be at least 1"); + if !(source < num_vertices) { + return Err(format!("source must be less than num_vertices ({num_vertices})").into()); + } + if !(sink < num_vertices) { + return Err(format!("sink must be less than num_vertices ({num_vertices})").into()); + } + if source == sink { + return Err("source and sink must be distinct".into()); + } + if requirement == 0 { + return Err("requirement must be at least 1".into()); + } for (edge_index, (&lower, &upper)) in lower_bounds.iter().zip(&capacities).enumerate() { - assert!( - lower <= upper, - "lower bound at edge {edge_index} must be at most its capacity" - ); + if !(lower <= upper) { + return Err(format!( + "lower bound at edge {edge_index} must be at most its capacity" + ) + .into()); + } } - Self { + Ok(Self { graph, capacities, lower_bounds, source, sink, requirement, - } + }) } pub fn graph(&self) -> &SimpleGraph { @@ -305,8 +270,12 @@ impl Problem for UndirectedFlowLowerBounds { } impl crate::solvers::BruteForceProblem for UndirectedFlowLowerBounds { - fn dimensions(&self) -> Vec { - vec![2; self.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -322,17 +291,21 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "undirected_flow_lower_bounds", - instance: Box::new(UndirectedFlowLowerBounds::new( - SimpleGraph::new( - 6, - vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 4), (3, 5), (4, 5)], - ), - vec![2, 2, 2, 2, 1, 3, 2], - vec![1, 1, 0, 0, 1, 0, 1], - 0, - 5, - 3, - )), + instance: Box::new( + UndirectedFlowLowerBounds::new( + SimpleGraph::new( + 6, + vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 4), (3, 5), (4, 5)], + ) + .unwrap(), + vec![2, 2, 2, 2, 1, 3, 2], + vec![1, 1, 0, 0, 1, 0, 1], + 0, + 5, + 3, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![false, false, false, false, false, false, false]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/graph/undirected_two_commodity_integral_flow.rs b/src/models/graph/undirected_two_commodity_integral_flow.rs index ad8a584f1..88efe73c8 100644 --- a/src/models/graph/undirected_two_commodity_integral_flow.rs +++ b/src/models/graph/undirected_two_commodity_integral_flow.rs @@ -30,6 +30,7 @@ inventory::submit! { /// - `f2(u, v)` /// - `f2(v, u)` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "UndirectedTwoCommodityIntegralFlowData")] pub struct UndirectedTwoCommodityIntegralFlow { graph: SimpleGraph, capacities: Vec, @@ -41,6 +42,34 @@ pub struct UndirectedTwoCommodityIntegralFlow { requirement_2: i64, } +#[derive(Deserialize)] +struct UndirectedTwoCommodityIntegralFlowData { + graph: SimpleGraph, + capacities: Vec, + source_1: usize, + sink_1: usize, + source_2: usize, + sink_2: usize, + requirement_1: i64, + requirement_2: i64, +} + +impl TryFrom for UndirectedTwoCommodityIntegralFlow { + type Error = crate::registry::ConstructionError; + fn try_from(data: UndirectedTwoCommodityIntegralFlowData) -> Result { + Self::new( + data.graph, + data.capacities, + data.source_1, + data.sink_1, + data.source_2, + data.sink_2, + data.requirement_1, + data.requirement_2, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct UndirectedTwoCommodityIntegralFlowCreateSpec { /// Undirected graph edges. @@ -79,41 +108,16 @@ impl TryFrom for UndirectedTwoComm .transpose()? .unwrap_or(0); let count = spec.num_vertices.unwrap_or(inferred); - if count < inferred { - return Err("num_vertices is too small for graph endpoints".into()); - } - if spec.capacities.len() != spec.graph.len() { - return Err("capacities length must match graph edge count".into()); - } - for &capacity in &spec.capacities { - if usize::try_from(capacity) - .ok() - .and_then(|v| v.checked_add(1)) - .is_none() - { - return Err("capacity is too large for this platform".into()); - } - } - for (label, vertex) in [ - ("source_1", spec.source_1), - ("sink_1", spec.sink_1), - ("source_2", spec.source_2), - ("sink_2", spec.sink_2), - ] { - if vertex >= count { - return Err(format!("{label} must be less than num_vertices").into()); - } - } - Ok(Self { - graph: SimpleGraph::new(count, spec.graph), - capacities: spec.capacities, - source_1: spec.source_1, - sink_1: spec.sink_1, - source_2: spec.source_2, - sink_2: spec.sink_2, - requirement_1: spec.requirement_1, - requirement_2: spec.requirement_2, - }) + Self::new( + SimpleGraph::new(count, spec.graph)?, + spec.capacities, + spec.source_1, + spec.sink_1, + spec.source_2, + spec.sink_2, + spec.requirement_1, + spec.requirement_2, + ) } } @@ -128,12 +132,10 @@ impl UndirectedTwoCommodityIntegralFlow { sink_2: usize, requirement_1: i64, requirement_2: i64, - ) -> Self { - assert_eq!( - capacities.len(), - graph.num_edges(), - "capacities length must match graph num_edges" - ); + ) -> Result { + if capacities.len() != graph.num_edges() { + return Err("capacities length must match graph num_edges".into()); + } let num_vertices = graph.num_vertices(); for (label, vertex) in [ @@ -142,23 +144,18 @@ impl UndirectedTwoCommodityIntegralFlow { ("source_2", source_2), ("sink_2", sink_2), ] { - assert!( - vertex < num_vertices, - "{label} must be less than num_vertices ({num_vertices})" - ); + if !(vertex < num_vertices) { + return Err( + format!("{label} must be less than num_vertices ({num_vertices})").into(), + ); + } } - for &capacity in &capacities { - let domain = usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)); - assert!( - domain.is_some(), - "edge capacities must fit into usize for dims()" - ); + if !(capacities.iter().all(|&capacity| capacity >= 0)) { + return Err("capacities must be nonnegative".into()); } - Self { + Ok(Self { graph, capacities, source_1, @@ -167,7 +164,7 @@ impl UndirectedTwoCommodityIntegralFlow { sink_2, requirement_1, requirement_2, - } + }) } pub fn graph(&self) -> &SimpleGraph { @@ -228,13 +225,6 @@ impl UndirectedTwoCommodityIntegralFlow { self.num_edges() * 4 } - fn domain_size(capacity: i64) -> usize { - usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)) - .expect("capacity already validated to fit into usize") - } - fn edge_flows(&self, config: &[usize], edge_index: usize) -> Option<[usize; 4]> { let start = edge_index.checked_mul(4)?; Some([ @@ -403,14 +393,14 @@ impl Problem for UndirectedTwoCommodityIntegralFlow { } impl crate::solvers::BruteForceProblem for UndirectedTwoCommodityIntegralFlow { - fn dimensions(&self) -> Vec { - self.capacities - .iter() - .flat_map(|&capacity| { - let domain = Self::domain_size(capacity); - std::iter::repeat_n(domain, 4) - }) - .collect() + fn num_variables(&self) -> Result { + Ok(4 * self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from( + i128::from(self.capacities[variable / 4]) + 1, + )?) } } @@ -426,16 +416,19 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "undirected_two_commodity_integral_flow", - instance: Box::new(UndirectedTwoCommodityIntegralFlow::new( - SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]), - vec![1, 1, 2], - 0, - 3, - 1, - 3, - 1, - 1, - )), + instance: Box::new( + UndirectedTwoCommodityIntegralFlow::new( + SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]).unwrap(), + vec![1, 1, 2], + 0, + 3, + 1, + 3, + 1, + 1, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/misc/additional_key.rs b/src/models/misc/additional_key.rs index d15016c1b..adbe93688 100644 --- a/src/models/misc/additional_key.rs +++ b/src/models/misc/additional_key.rs @@ -55,12 +55,13 @@ inventory::submit! { /// vec![(vec![0], vec![1, 2])], /// vec![0, 1, 2], /// vec![], -/// ); +/// ).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "AdditionalKeyData")] pub struct AdditionalKey { num_attributes: usize, dependencies: Vec<(Vec, Vec)>, @@ -68,55 +69,81 @@ pub struct AdditionalKey { known_keys: Vec>, } +#[derive(Deserialize)] +struct AdditionalKeyData { + num_attributes: usize, + dependencies: Vec<(Vec, Vec)>, + relation_attrs: Vec, + known_keys: Vec>, +} + +impl TryFrom for AdditionalKey { + type Error = crate::registry::ConstructionError; + fn try_from(data: AdditionalKeyData) -> Result { + Self::new( + data.num_attributes, + data.dependencies, + data.relation_attrs, + data.known_keys, + ) + } +} + impl AdditionalKey { /// Create a new AdditionalKey instance. /// - /// # Panics + /// # Errors /// - /// Panics if any attribute index is >= `num_attributes`, or if + /// Returns an error if any attribute index is >= `num_attributes`, or if /// `relation_attrs` contains duplicates. pub fn new( num_attributes: usize, dependencies: Vec<(Vec, Vec)>, relation_attrs: Vec, known_keys: Vec>, - ) -> Self { + ) -> Result { // Validate all attribute indices for &a in &relation_attrs { - assert!( - a < num_attributes, - "relation_attrs element {a} >= num_attributes {num_attributes}" - ); + if !(a < num_attributes) { + return Err(format!( + "relation_attrs element {a} >= num_attributes {num_attributes}" + ) + .into()); + } } // Validate relation_attrs uniqueness let mut sorted_ra = relation_attrs.clone(); sorted_ra.sort_unstable(); sorted_ra.dedup(); - assert_eq!( - sorted_ra.len(), - relation_attrs.len(), - "relation_attrs contains duplicates" - ); + if sorted_ra.len() != relation_attrs.len() { + return Err("relation_attrs contains duplicates".into()); + } for (lhs, rhs) in &dependencies { for &a in lhs { - assert!( - a < num_attributes, - "dependency lhs attribute {a} >= num_attributes {num_attributes}" - ); + if !(a < num_attributes) { + return Err(format!( + "dependency lhs attribute {a} >= num_attributes {num_attributes}" + ) + .into()); + } } for &a in rhs { - assert!( - a < num_attributes, - "dependency rhs attribute {a} >= num_attributes {num_attributes}" - ); + if !(a < num_attributes) { + return Err(format!( + "dependency rhs attribute {a} >= num_attributes {num_attributes}" + ) + .into()); + } } } for key in &known_keys { for &a in key { - assert!( - a < num_attributes, - "known_keys attribute {a} >= num_attributes {num_attributes}" - ); + if !(a < num_attributes) { + return Err(format!( + "known_keys attribute {a} >= num_attributes {num_attributes}" + ) + .into()); + } } } // Sort known_keys entries internally for consistent comparison @@ -127,12 +154,12 @@ impl AdditionalKey { k }) .collect(); - Self { + Ok(Self { num_attributes, dependencies, relation_attrs, known_keys, - } + }) } /// Returns the number of attributes in the universal set A. @@ -265,8 +292,12 @@ impl Problem for AdditionalKey { } impl crate::solvers::BruteForceProblem for AdditionalKey { - fn dimensions(&self) -> Vec { - vec![2; self.relation_attrs.len()] + fn num_variables(&self) -> Result { + Ok(self.relation_attrs.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -282,18 +313,21 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "additional_key", - instance: Box::new(AdditionalKey::new( - 6, - vec![ - (vec![0, 1], vec![2, 3]), - (vec![2, 3], vec![4, 5]), - (vec![4, 5], vec![0, 1]), - (vec![0, 2], vec![3]), - (vec![3, 5], vec![1]), - ], - vec![0, 1, 2, 3, 4, 5], - vec![vec![0, 1], vec![2, 3], vec![4, 5]], - )), + instance: Box::new( + AdditionalKey::new( + 6, + vec![ + (vec![0, 1], vec![2, 3]), + (vec![2, 3], vec![4, 5]), + (vec![4, 5], vec![0, 1]), + (vec![0, 2], vec![3]), + (vec![3, 5], vec![1]), + ], + vec![0, 1, 2, 3, 4, 5], + vec![vec![0, 1], vec![2, 3], vec![4, 5]], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![true, false, true, false, false, false]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/misc/betweenness.rs b/src/models/misc/betweenness.rs index bf55a1c12..64778ea16 100644 --- a/src/models/misc/betweenness.rs +++ b/src/models/misc/betweenness.rs @@ -170,8 +170,12 @@ impl Problem for Betweenness { } impl crate::solvers::BruteForceProblem for Betweenness { - fn dimensions(&self) -> Vec { - vec![self.num_elements; self.num_elements] + fn num_variables(&self) -> Result { + Ok(self.num_elements) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_elements) } } diff --git a/src/models/misc/bin_packing.rs b/src/models/misc/bin_packing.rs index 0f952f549..d307109c4 100644 --- a/src/models/misc/bin_packing.rs +++ b/src/models/misc/bin_packing.rs @@ -155,9 +155,12 @@ where W: WeightElement + crate::variant::VariantParam, W::Sum: PartialOrd, { - fn dimensions(&self) -> Vec { - let n = self.sizes.len(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.sizes.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.sizes.len()) } } diff --git a/src/models/misc/boyce_codd_normal_form_violation.rs b/src/models/misc/boyce_codd_normal_form_violation.rs index a09631428..1ed177960 100644 --- a/src/models/misc/boyce_codd_normal_form_violation.rs +++ b/src/models/misc/boyce_codd_normal_form_violation.rs @@ -50,7 +50,7 @@ inventory::submit! { /// (vec![3, 4], vec![5]), /// ], /// vec![0, 1, 2, 3, 4, 5], -/// ); +/// ).unwrap(); /// let solver = BruteForce::new(); /// // X = {2}: closure = {2, 3}, y=3 ∈ closure, z=0 ∉ closure → BCNF violation /// assert!(problem @@ -58,6 +58,7 @@ inventory::submit! { /// .unwrap()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "BoyceCoddNormalFormViolationData")] pub struct BoyceCoddNormalFormViolation { /// Total number of attributes (elements are `0..num_attributes`). num_attributes: usize, @@ -67,6 +68,24 @@ pub struct BoyceCoddNormalFormViolation { target_subset: Vec, } +#[derive(Deserialize)] +struct BoyceCoddNormalFormViolationData { + num_attributes: usize, + functional_deps: Vec<(Vec, Vec)>, + target_subset: Vec, +} + +impl TryFrom for BoyceCoddNormalFormViolation { + type Error = crate::registry::ConstructionError; + fn try_from(data: BoyceCoddNormalFormViolationData) -> Result { + Self::new( + data.num_attributes, + data.functional_deps, + data.target_subset, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct BoyceCoddNormalFormViolationCreateSpec { /// Total number of attributes in A. @@ -82,41 +101,16 @@ impl TryFrom for BoyceCoddNormalFormViol type Error = crate::registry::ConstructionError; fn try_from(spec: BoyceCoddNormalFormViolationCreateSpec) -> Result { - if spec.target.is_empty() { - return Err("target must be non-empty".to_string().into()); - } - for (dependency_index, (lhs, rhs)) in spec.subsets.iter().enumerate() { - if lhs.is_empty() { - return Err(format!("subsets[{dependency_index}] has an empty left side").into()); - } - if let Some(&attribute) = lhs - .iter() - .chain(rhs) - .find(|&&attribute| attribute >= spec.n) - { - return Err(format!( - "subsets[{dependency_index}] contains attribute {attribute} outside universe of size {}", - spec.n - ).into()); - } - } - if let Some(&attribute) = spec.target.iter().find(|&&attribute| attribute >= spec.n) { - return Err(format!( - "target contains attribute {attribute} outside universe of size {}", - spec.n - ) - .into()); - } - Ok(Self::new(spec.n, spec.subsets, spec.target)) + Self::new(spec.n, spec.subsets, spec.target) } } impl BoyceCoddNormalFormViolation { /// Create a new Boyce-Codd Normal Form Violation instance. /// - /// # Panics + /// # Errors /// - /// Panics if any attribute index in `functional_deps` or `target_subset` is + /// Returns an error if any attribute index in `functional_deps` or `target_subset` is /// out of range (≥ `num_attributes`), if `target_subset` is empty, or if any /// functional dependency has an empty LHS. /// @@ -128,28 +122,24 @@ impl BoyceCoddNormalFormViolation { num_attributes: usize, functional_deps: Vec<(Vec, Vec)>, target_subset: Vec, - ) -> Self { - assert!(!target_subset.is_empty(), "target_subset must be non-empty"); + ) -> Result { + if target_subset.is_empty() { + return Err("target_subset must be non-empty".into()); + } let mut functional_deps = functional_deps; for (fd_index, (lhs, rhs)) in functional_deps.iter_mut().enumerate() { - assert!( - !lhs.is_empty(), - "Functional dependency {} has an empty LHS", - fd_index - ); + if lhs.is_empty() { + return Err(format!("Functional dependency {} has an empty LHS", fd_index).into()); + } lhs.sort_unstable(); lhs.dedup(); rhs.sort_unstable(); rhs.dedup(); for &attr in lhs.iter().chain(rhs.iter()) { - assert!( - attr < num_attributes, - "Functional dependency {} contains attribute {} which is out of range (num_attributes = {})", - fd_index, - attr, - num_attributes - ); + if !(attr < num_attributes) { + return Err(format!("Functional dependency {} contains attribute {} which is out of range (num_attributes = {})", fd_index, attr, num_attributes).into()); + } } } @@ -157,19 +147,16 @@ impl BoyceCoddNormalFormViolation { target_subset.sort_unstable(); target_subset.dedup(); for &attr in &target_subset { - assert!( - attr < num_attributes, - "target_subset contains attribute {} which is out of range (num_attributes = {})", - attr, - num_attributes - ); + if !(attr < num_attributes) { + return Err(format!("target_subset contains attribute {} which is out of range (num_attributes = {})", attr, num_attributes).into()); + } } - Self { + Ok(Self { num_attributes, functional_deps, target_subset, - } + }) } /// Return the total number of attributes. @@ -269,8 +256,12 @@ impl Problem for BoyceCoddNormalFormViolation { } impl crate::solvers::BruteForceProblem for BoyceCoddNormalFormViolation { - fn dimensions(&self) -> Vec { - vec![2; self.target_subset.len()] + fn num_variables(&self) -> Result { + Ok(self.target_subset.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -286,15 +277,18 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "boyce_codd_normal_form_violation", - instance: Box::new(BoyceCoddNormalFormViolation::new( - 6, - vec![ - (vec![0, 1], vec![2]), - (vec![2], vec![3]), - (vec![3, 4], vec![5]), - ], - vec![0, 1, 2, 3, 4, 5], - )), + instance: Box::new( + BoyceCoddNormalFormViolation::new( + 6, + vec![ + (vec![0, 1], vec![2]), + (vec![2], vec![3]), + (vec![3, 4], vec![5]), + ], + vec![0, 1, 2, 3, 4, 5], + ) + .unwrap(), + ), // X={2}: closure={2,3}, y=3 in closure, z=0 not in closure -> violation optimal_config: serde_json::json!(vec![false, false, true, false, false, false]), optimal_value: serde_json::json!(true), diff --git a/src/models/misc/capacity_assignment.rs b/src/models/misc/capacity_assignment.rs index f397cdb4d..8aa69a2d7 100644 --- a/src/models/misc/capacity_assignment.rs +++ b/src/models/misc/capacity_assignment.rs @@ -27,6 +27,7 @@ inventory::submit! { /// with respect to the ordered capacity list. The objective is to minimize /// total cost subject to a delay budget constraint. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "CapacityAssignmentCreateSpec")] pub struct CapacityAssignment { capacities: Vec, cost: Vec>, @@ -48,40 +49,7 @@ struct CapacityAssignmentCreateSpec { impl TryFrom for CapacityAssignment { type Error = crate::registry::ConstructionError; fn try_from(spec: CapacityAssignmentCreateSpec) -> Result { - if spec.capacities.is_empty() { - return Err("capacities must be non-empty".into()); - } - if spec.capacities.contains(&0) { - return Err("capacities must be positive".into()); - } - if !spec.capacities.windows(2).all(|w| w[0] < w[1]) { - return Err("capacities must be strictly increasing".into()); - } - if spec.cost.len() != spec.delay.len() { - return Err("cost and delay must have the same number of links".into()); - } - for (i, row) in spec.cost.iter().enumerate() { - if row.len() != spec.capacities.len() { - return Err(format!("cost row {i} length must match capacities length").into()); - } - if !row.windows(2).all(|w| w[0] <= w[1]) { - return Err(format!("cost row {i} must be non-decreasing").into()); - } - } - for (i, row) in spec.delay.iter().enumerate() { - if row.len() != spec.capacities.len() { - return Err(format!("delay row {i} length must match capacities length").into()); - } - if !row.windows(2).all(|w| w[0] >= w[1]) { - return Err(format!("delay row {i} must be non-increasing").into()); - } - } - Ok(Self { - capacities: spec.capacities, - cost: spec.cost, - delay: spec.delay, - delay_budget: spec.delay_budget, - }) + Self::new(spec.capacities, spec.cost, spec.delay, spec.delay_budget) } } @@ -92,52 +60,44 @@ impl CapacityAssignment { cost: Vec>, delay: Vec>, delay_budget: i64, - ) -> Self { - assert!(!capacities.is_empty(), "capacities must be non-empty"); - assert!( - capacities.iter().all(|&capacity| capacity > 0), - "capacities must be positive" - ); - assert!( - capacities.windows(2).all(|w| w[0] < w[1]), - "capacities must be strictly increasing" - ); - assert_eq!( - cost.len(), - delay.len(), - "cost and delay must have the same number of links" - ); + ) -> Result { + if capacities.is_empty() { + return Err("capacities must be non-empty".into()); + } + if !(capacities.iter().all(|&capacity| capacity > 0)) { + return Err("capacities must be positive".into()); + } + if !(capacities.windows(2).all(|w| w[0] < w[1])) { + return Err("capacities must be strictly increasing".into()); + } + if cost.len() != delay.len() { + return Err("cost and delay must have the same number of links".into()); + } let num_capacities = capacities.len(); for (link, row) in cost.iter().enumerate() { - assert_eq!( - row.len(), - num_capacities, - "cost row {link} length must match capacities length" - ); - assert!( - row.windows(2).all(|w| w[0] <= w[1]), - "cost row {link} must be non-decreasing" - ); + if row.len() != num_capacities { + return Err(format!("cost row {link} length must match capacities length").into()); + } + if !(row.windows(2).all(|w| w[0] <= w[1])) { + return Err(format!("cost row {link} must be non-decreasing").into()); + } } for (link, row) in delay.iter().enumerate() { - assert_eq!( - row.len(), - num_capacities, - "delay row {link} length must match capacities length" - ); - assert!( - row.windows(2).all(|w| w[0] >= w[1]), - "delay row {link} must be non-increasing" - ); + if row.len() != num_capacities { + return Err(format!("delay row {link} length must match capacities length").into()); + } + if !(row.windows(2).all(|w| w[0] >= w[1])) { + return Err(format!("delay row {link} must be non-increasing").into()); + } } - Self { + Ok(Self { capacities, cost, delay, delay_budget, - } + }) } /// Number of communication links. @@ -245,8 +205,12 @@ impl Problem for CapacityAssignment { } impl crate::solvers::BruteForceProblem for CapacityAssignment { - fn dimensions(&self) -> Vec { - vec![self.num_capacities(); self.num_links()] + fn num_variables(&self) -> Result { + Ok(self.num_links()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_capacities()) } } @@ -262,12 +226,15 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "capacity_assignment", - instance: Box::new(CapacityAssignment::new( - vec![1, 2, 3], - vec![vec![1, 3, 6], vec![2, 4, 7], vec![1, 2, 5]], - vec![vec![8, 4, 1], vec![7, 3, 1], vec![6, 3, 1]], - 12, - )), + instance: Box::new( + CapacityAssignment::new( + vec![1, 2, 3], + vec![vec![1, 3, 6], vec![2, 4, 7], vec![1, 2, 5]], + vec![vec![8, 4, 1], vec![7, 3, 1], vec![6, 3, 1]], + 12, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![1, 1, 1]), optimal_value: serde_json::json!(9), }] diff --git a/src/models/misc/closest_string.rs b/src/models/misc/closest_string.rs index 722d23dfd..9c59bf9eb 100644 --- a/src/models/misc/closest_string.rs +++ b/src/models/misc/closest_string.rs @@ -46,46 +46,61 @@ inventory::submit! { /// syntactically feasible; the objective is its worst-case Hamming distance /// to the input strings. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ClosestStringData")] pub struct ClosestString { alphabet_size: usize, strings: Vec>, } +#[derive(Deserialize)] +struct ClosestStringData { + alphabet_size: usize, + strings: Vec>, +} + +impl TryFrom for ClosestString { + type Error = crate::registry::ConstructionError; + + fn try_from(data: ClosestStringData) -> Result { + Self::new(data.alphabet_size, data.strings) + } +} + impl ClosestString { /// Create a new `ClosestString` instance. /// - /// # Panics + /// # Errors /// - /// Panics if: + /// Returns an error if: /// - `strings` is empty (the problem requires at least one input string), /// - input strings do not all have the same length, /// - `alphabet_size == 0` while any input string is non-empty, /// - any symbol in any input string is `>= alphabet_size`. - pub fn new(alphabet_size: usize, strings: Vec>) -> Self { - assert!( - !strings.is_empty(), - "ClosestString requires at least one input string" - ); + pub fn new( + alphabet_size: usize, + strings: Vec>, + ) -> Result { + if strings.is_empty() { + return Err("ClosestString requires at least one input string".into()); + } let string_length = strings[0].len(); - assert!( - strings.iter().all(|s| s.len() == string_length), - "all input strings must have the same length" - ); - assert!( - alphabet_size > 0 || string_length == 0, - "alphabet_size must be > 0 when input strings are non-empty" - ); - assert!( - strings - .iter() - .flat_map(|s| s.iter()) - .all(|&symbol| symbol < alphabet_size), - "input symbols must be less than alphabet_size" - ); - Self { + if !(strings.iter().all(|s| s.len() == string_length)) { + return Err("all input strings must have the same length".into()); + } + if !(alphabet_size > 0 || string_length == 0) { + return Err("alphabet_size must be > 0 when input strings are non-empty".into()); + } + if !(strings + .iter() + .flat_map(|s| s.iter()) + .all(|&symbol| symbol < alphabet_size)) + { + return Err("input symbols must be less than alphabet_size".into()); + } + Ok(Self { alphabet_size, strings, - } + }) } /// Returns the alphabet size `q`. @@ -169,8 +184,12 @@ impl Problem for ClosestString { } impl crate::solvers::BruteForceProblem for ClosestString { - fn dimensions(&self) -> Vec { - vec![self.alphabet_size; self.string_length()] + fn num_variables(&self) -> Result { + Ok(self.string_length()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.alphabet_size) } } @@ -186,10 +205,13 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "closest_string", - instance: Box::new(ClosestString::new( - 2, - vec![vec![0, 0, 0], vec![0, 1, 1], vec![1, 0, 1], vec![1, 1, 0]], - )), + instance: Box::new( + ClosestString::new( + 2, + vec![vec![0, 0, 0], vec![0, 1, 1], vec![1, 0, 1], vec![1, 1, 0]], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![0, 0, 0]), optimal_value: serde_json::json!(2), }] diff --git a/src/models/misc/closest_substring.rs b/src/models/misc/closest_substring.rs index 56a504e5a..04680a18f 100644 --- a/src/models/misc/closest_substring.rs +++ b/src/models/misc/closest_substring.rs @@ -113,11 +113,6 @@ impl ClosestSubstring { .map(|string| string.len() - substring_length + 1) .try_fold(0_usize, usize::checked_add) .ok_or("total number of windows exceeds usize")?; - strings - .iter() - .map(|string| string.len() - substring_length + 1) - .try_fold(1_usize, usize::checked_mul) - .ok_or("window-choice count exceeds usize")?; Ok(Self { alphabet_size, strings, @@ -157,16 +152,6 @@ impl ClosestSubstring { .map(|s| s.len() - self.substring_length + 1) .sum() } - - /// Returns `prod_i W_i`, the number of distinct window-selection tuples. - /// - pub fn num_window_choice_product(&self) -> usize { - self.strings - .iter() - .map(|s| s.len() - self.substring_length + 1) - .try_fold(1usize, usize::checked_mul) - .expect("validated window-choice count must fit usize") - } } impl Problem for ClosestSubstring { @@ -180,7 +165,6 @@ impl Problem for ClosestSubstring { ("substring_length", substring_length), ("total_length", total_length), ("total_num_windows", total_num_windows), - ("num_window_choice_product", num_window_choice_product), ]; fn variant() -> Vec<(&'static str, &'static str)> { @@ -235,16 +219,22 @@ impl Problem for ClosestSubstring { } impl crate::solvers::BruteForceProblem for ClosestSubstring { - fn dimensions(&self) -> Vec { - let ell = self.substring_length; - let mut dims = vec![self.alphabet_size; ell]; - dims.extend(self.strings.iter().map(|s| s.len() - ell + 1)); - dims + fn num_variables(&self) -> Result { + Ok(self.substring_length + self.strings.len()) + } + + fn dimension(&self, variable: usize) -> Result { + if variable < self.substring_length { + Ok(self.alphabet_size) + } else { + Ok(self.strings[variable - self.substring_length].len() - self.substring_length + 1) + } } } crate::declare_variants! { - default ClosestSubstring => "alphabet_size ^ substring_length * num_window_choice_product", + // AM-GM bounds the product of window counts by their mean raised to num_strings. + default ClosestSubstring => "alphabet_size ^ substring_length * (total_num_windows / num_strings)^num_strings", } crate::register_brute_force! { diff --git a/src/models/misc/clustering.rs b/src/models/misc/clustering.rs index e87998c73..221ff642f 100644 --- a/src/models/misc/clustering.rs +++ b/src/models/misc/clustering.rs @@ -53,12 +53,13 @@ inventory::submit! { /// vec![3, 3, 0, 1], /// vec![3, 3, 1, 0], /// ]; -/// let problem = Clustering::new(distances, 2, 1); +/// let problem = Clustering::new(distances, 2, 1).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ClusteringData")] pub struct Clustering { /// Symmetric distance matrix with zero diagonal. distances: Vec>, @@ -68,47 +69,68 @@ pub struct Clustering { diameter_bound: i64, } +#[derive(Deserialize)] +struct ClusteringData { + distances: Vec>, + num_clusters: usize, + diameter_bound: i64, +} + +impl TryFrom for Clustering { + type Error = crate::registry::ConstructionError; + + fn try_from(data: ClusteringData) -> Result { + Self::new(data.distances, data.num_clusters, data.diameter_bound) + } +} + impl Clustering { /// Create a new Clustering instance. /// - /// # Panics + /// # Errors /// - /// Panics if: + /// Returns an error if: /// - `distances` is empty /// - `distances` is not square /// - `distances` is not symmetric /// - diagonal entries are not zero /// - `num_clusters` is zero - pub fn new(distances: Vec>, num_clusters: usize, diameter_bound: i64) -> Self { + pub fn new( + distances: Vec>, + num_clusters: usize, + diameter_bound: i64, + ) -> Result { let n = distances.len(); - assert!(n > 0, "Clustering requires at least one element"); - assert!(num_clusters > 0, "num_clusters must be at least 1"); + if !(n > 0) { + return Err("Clustering requires at least one element".into()); + } + if num_clusters == 0 { + return Err("num_clusters must be at least 1".into()); + } for (i, row) in distances.iter().enumerate() { - assert_eq!( - row.len(), - n, - "Distance matrix must be square: row {i} has {} columns, expected {n}", - row.len() - ); - assert_eq!( - distances[i][i], 0, - "Diagonal entry distances[{i}][{i}] must be 0" - ); + if row.len() != n { + return Err(format!( + "Distance matrix must be square: row {i} has {} columns, expected {n}", + row.len() + ) + .into()); + } + if distances[i][i] != 0 { + return Err(format!("Diagonal entry distances[{i}][{i}] must be 0").into()); + } } for (i, row_i) in distances.iter().enumerate() { for j in (i + 1)..n { - assert_eq!( - row_i[j], distances[j][i], - "Distance matrix must be symmetric: distances[{i}][{j}] = {} != distances[{j}][{i}] = {}", - row_i[j], distances[j][i] - ); + if row_i[j] != distances[j][i] { + return Err(format!("Distance matrix must be symmetric: distances[{i}][{j}] = {} != distances[{j}][{i}] = {}", row_i[j], distances[j][i]).into()); + } } } - Self { + Ok(Self { distances, num_clusters, diameter_bound, - } + }) } /// Returns the distance matrix. @@ -192,8 +214,12 @@ impl Problem for Clustering { } impl crate::solvers::BruteForceProblem for Clustering { - fn dimensions(&self) -> Vec { - vec![self.num_clusters; self.num_elements()] + fn num_variables(&self) -> Result { + Ok(self.num_elements()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_clusters) } } @@ -220,7 +246,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec, @@ -83,6 +84,26 @@ pub struct ConjunctiveBooleanQuery { conjuncts: Vec<(usize, Vec)>, } +#[derive(Deserialize)] +struct ConjunctiveBooleanQueryData { + domain_size: usize, + relations: Vec, + num_variables: usize, + conjuncts: Vec<(usize, Vec)>, +} + +impl TryFrom for ConjunctiveBooleanQuery { + type Error = crate::registry::ConstructionError; + fn try_from(data: ConjunctiveBooleanQueryData) -> Result { + Self::new( + data.domain_size, + data.relations, + data.num_variables, + data.conjuncts, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct ConjunctiveBooleanQueryCreateSpec { /// Size of the finite domain. @@ -111,68 +132,21 @@ impl TryFrom for ConjunctiveBooleanQuery { } } - for (relation_index, relation) in spec.relations.iter().enumerate() { - for (tuple_index, tuple) in relation.tuples.iter().enumerate() { - if tuple.len() != relation.arity { - return Err(format!( - "relation {relation_index} tuple {tuple_index} has length {}, expected arity {}", - tuple.len(), - relation.arity - ).into()); - } - for (entry_index, &value) in tuple.iter().enumerate() { - if value >= spec.domain_size { - return Err(format!( - "relation {relation_index} tuple {tuple_index} entry {entry_index} is {value}, must be less than domain size {}", - spec.domain_size - ).into()); - } - } - } - } - - for (conjunct_index, (relation_index, args)) in spec.conjuncts.iter().enumerate() { - let relation = spec.relations.get(*relation_index).ok_or_else(|| { - format!( - "conjunct {conjunct_index} relation index {relation_index} is out of range for {} relations", - spec.relations.len() - ) - })?; - if args.len() != relation.arity { - return Err(format!( - "conjunct {conjunct_index} has {} arguments, expected arity {}", - args.len(), - relation.arity - ) - .into()); - } - for (argument_index, arg) in args.iter().enumerate() { - if let QueryArg::Constant(value) = arg { - if *value >= spec.domain_size { - return Err(format!( - "conjunct {conjunct_index} argument {argument_index} constant {value} must be less than domain size {}", - spec.domain_size - ).into()); - } - } - } - } - - Ok(Self { - domain_size: spec.domain_size, - relations: spec.relations, + Self::new( + spec.domain_size, + spec.relations, num_variables, - conjuncts: spec.conjuncts, - }) + spec.conjuncts, + ) } } impl ConjunctiveBooleanQuery { /// Create a new ConjunctiveBooleanQuery instance. /// - /// # Panics + /// # Errors /// - /// Panics if: + /// Returns an error if: /// - Any relation's tuples have incorrect arity /// - Any tuple entry is >= domain_size /// - Any conjunct references a non-existent relation @@ -184,58 +158,64 @@ impl ConjunctiveBooleanQuery { relations: Vec, num_variables: usize, conjuncts: Vec<(usize, Vec)>, - ) -> Self { + ) -> Result { for (i, rel) in relations.iter().enumerate() { for (j, tuple) in rel.tuples.iter().enumerate() { - assert!( - tuple.len() == rel.arity, - "Relation {i}: tuple {j} has length {}, expected arity {}", - tuple.len(), - rel.arity - ); + if !(tuple.len() == rel.arity) { + return Err(format!( + "Relation {i}: tuple {j} has length {}, expected arity {}", + tuple.len(), + rel.arity + ) + .into()); + } for (k, &val) in tuple.iter().enumerate() { - assert!( - val < domain_size, - "Relation {i}: tuple {j}, entry {k} is {val}, must be < {domain_size}" - ); + if !(val < domain_size) { + return Err(format!( + "Relation {i}: tuple {j}, entry {k} is {val}, must be < {domain_size}" + ) + .into()); + } } } } for (i, (rel_idx, args)) in conjuncts.iter().enumerate() { - assert!( - *rel_idx < relations.len(), - "Conjunct {i}: relation index {rel_idx} out of range (have {} relations)", - relations.len() - ); - assert!( - args.len() == relations[*rel_idx].arity, - "Conjunct {i}: has {} args, expected arity {}", - args.len(), - relations[*rel_idx].arity - ); + if !(*rel_idx < relations.len()) { + return Err(format!( + "Conjunct {i}: relation index {rel_idx} out of range (have {} relations)", + relations.len() + ) + .into()); + } + if !(args.len() == relations[*rel_idx].arity) { + return Err(format!( + "Conjunct {i}: has {} args, expected arity {}", + args.len(), + relations[*rel_idx].arity + ) + .into()); + } for (k, arg) in args.iter().enumerate() { match arg { QueryArg::Variable(v) => { - assert!( - *v < num_variables, - "Conjunct {i}, arg {k}: Variable({v}) >= num_variables ({num_variables})" - ); + if !(*v < num_variables) { + return Err(format!("Conjunct {i}, arg {k}: Variable({v}) >= num_variables ({num_variables})").into()); + } } QueryArg::Constant(c) => { - assert!( - *c < domain_size, - "Conjunct {i}, arg {k}: Constant({c}) >= domain_size ({domain_size})" - ); + if !(*c < domain_size) { + return Err(format!("Conjunct {i}, arg {k}: Constant({c}) >= domain_size ({domain_size})").into()); + } } } } } - Self { + Ok(Self { domain_size, relations, num_variables, conjuncts, - } + }) } /// Returns the size of the finite domain. @@ -317,8 +297,12 @@ impl Problem for ConjunctiveBooleanQuery { } impl crate::solvers::BruteForceProblem for ConjunctiveBooleanQuery { - fn dimensions(&self) -> Vec { - vec![self.domain_size; self.num_variables] + fn num_variables(&self) -> Result { + Ok(self.num_variables) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.domain_size) } } @@ -336,32 +320,35 @@ pub(crate) fn canonical_model_example_specs() -> Vec)>, } +#[derive(Deserialize)] +struct ConjunctiveQueryFoldabilityData { + domain_size: usize, + num_distinguished: usize, + num_undistinguished: usize, + relation_arities: Vec, + query1_conjuncts: Vec<(usize, Vec)>, + query2_conjuncts: Vec<(usize, Vec)>, +} + +impl TryFrom for ConjunctiveQueryFoldability { + type Error = crate::registry::ConstructionError; + fn try_from(data: ConjunctiveQueryFoldabilityData) -> Result { + Self::new( + data.domain_size, + data.num_distinguished, + data.num_undistinguished, + data.relation_arities, + data.query1_conjuncts, + data.query2_conjuncts, + ) + } +} + impl ConjunctiveQueryFoldability { /// Create a new `ConjunctiveQueryFoldability` instance. /// @@ -113,9 +138,9 @@ impl ConjunctiveQueryFoldability { /// * `query1_conjuncts` – Atoms of Q1 as `(relation_index, args)` pairs. /// * `query2_conjuncts` – Atoms of Q2 as `(relation_index, args)` pairs. /// - /// # Panics + /// # Errors /// - /// Panics if: + /// Returns an error if: /// - Any atom references a relation index out of range. /// - Any atom has the wrong number of arguments for its relation's arity. /// - Any `Constant(i)` has `i >= domain_size`. @@ -128,7 +153,7 @@ impl ConjunctiveQueryFoldability { relation_arities: Vec, query1_conjuncts: Vec<(usize, Vec)>, query2_conjuncts: Vec<(usize, Vec)>, - ) -> Self { + ) -> Result { let instance = Self { domain_size, num_distinguished, @@ -137,55 +162,63 @@ impl ConjunctiveQueryFoldability { query1_conjuncts, query2_conjuncts, }; - instance.validate(); - instance + instance.validate()?; + Ok(instance) } - /// Validate the instance, panicking on any inconsistency. - fn validate(&self) { + /// Check relation arities and argument indices. + fn validate(&self) -> Result<(), crate::registry::ConstructionError> { for (query_name, conjuncts) in [ ("Q1", &self.query1_conjuncts), ("Q2", &self.query2_conjuncts), ] { for (atom_idx, (rel_idx, args)) in conjuncts.iter().enumerate() { - assert!( - *rel_idx < self.relation_arities.len(), - "Atom {atom_idx} of {query_name}: relation index {rel_idx} out of range \ + if !(*rel_idx < self.relation_arities.len()) { + return Err(format!( + "Atom {atom_idx} of {query_name}: relation index {rel_idx} out of range \ (num_relations = {})", - self.relation_arities.len() - ); + self.relation_arities.len() + ) + .into()); + }; let arity = self.relation_arities[*rel_idx]; - assert_eq!( - args.len(), - arity, - "Atom {atom_idx} of {query_name}: relation {rel_idx} has arity {arity} \ + if args.len() != arity { + return Err(format!( + "Atom {atom_idx} of {query_name}: relation {rel_idx} has arity {arity} \ but got {} arguments", - args.len() - ); + args.len() + ) + .into()); + }; for term in args { match term { - Term::Constant(i) => assert!( - *i < self.domain_size, - "Atom {atom_idx} of {query_name}: Constant({i}) out of range \ + Term::Constant(i) => { + if !(*i < self.domain_size) { + return Err(format!( + "Atom {atom_idx} of {query_name}: Constant({i}) out of range \ (domain_size = {})", - self.domain_size - ), - Term::Distinguished(i) => assert!( - *i < self.num_distinguished, - "Atom {atom_idx} of {query_name}: Distinguished({i}) out of range \ - (num_distinguished = {})", - self.num_distinguished - ), - Term::Undistinguished(i) => assert!( - *i < self.num_undistinguished, - "Atom {atom_idx} of {query_name}: Undistinguished({i}) out of range \ - (num_undistinguished = {})", - self.num_undistinguished - ), + self.domain_size + ) + .into()); + } + } + Term::Distinguished(i) => { + if !(*i < self.num_distinguished) { + return Err(format!("Atom {atom_idx} of {query_name}: Distinguished({i}) out of range \ + (num_distinguished = {})", self.num_distinguished).into()); + } + } + Term::Undistinguished(i) => { + if !(*i < self.num_undistinguished) { + return Err(format!("Atom {atom_idx} of {query_name}: Undistinguished({i}) out of range \ + (num_undistinguished = {})", self.num_undistinguished).into()); + } + } } } } } + Ok(()) } /// Returns the size of the finite domain D. @@ -325,9 +358,22 @@ impl Problem for ConjunctiveQueryFoldability { impl crate::solvers::BruteForceProblem for ConjunctiveQueryFoldability { /// Each undistinguished variable can map to any element of `D ∪ X ∪ Y`. - fn dimensions(&self) -> Vec { - let range = self.domain_size + self.num_distinguished + self.num_undistinguished; - vec![range; self.num_undistinguished] + fn num_variables(&self) -> Result { + Ok(self.num_undistinguished) + } + + fn dimension(&self, _variable: usize) -> Result { + ((self.domain_size) + .checked_add(self.num_distinguished) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a coordinate cardinality".into(), + ) + })?) + .checked_add(self.num_undistinguished) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } @@ -350,23 +396,26 @@ pub(crate) fn canonical_model_example_specs() -> Vec, @@ -104,6 +105,26 @@ pub struct ConsistencyOfDatabaseFrequencyTables { known_values: Vec, } +#[derive(Deserialize)] +struct ConsistencyOfDatabaseFrequencyTablesData { + num_objects: usize, + attribute_domains: Vec, + frequency_tables: Vec, + known_values: Vec, +} + +impl TryFrom for ConsistencyOfDatabaseFrequencyTables { + type Error = crate::registry::ConstructionError; + fn try_from(data: ConsistencyOfDatabaseFrequencyTablesData) -> Result { + Self::new( + data.num_objects, + data.attribute_domains, + data.frequency_tables, + data.known_values, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct ConsistencyOfDatabaseFrequencyTablesCreateSpec { /// Number of database objects. @@ -124,19 +145,12 @@ impl TryFrom { type Error = crate::registry::ConstructionError; fn try_from(spec: ConsistencyOfDatabaseFrequencyTablesCreateSpec) -> Result { - let known_values = spec.known_values.unwrap_or_default(); - validate_cdft_create( + Self::new( spec.num_objects, - &spec.attribute_domains, - &spec.frequency_tables, - &known_values, - )?; - Ok(Self { - num_objects: spec.num_objects, - attribute_domains: spec.attribute_domains, - frequency_tables: spec.frequency_tables, - known_values, - }) + spec.attribute_domains, + spec.frequency_tables, + spec.known_values.unwrap_or_default(), + ) } } @@ -219,21 +233,20 @@ impl ConsistencyOfDatabaseFrequencyTables { attribute_domains: Vec, frequency_tables: Vec, known_values: Vec, - ) -> Self { + ) -> Result { validate_cdft_create( num_objects, &attribute_domains, &frequency_tables, &known_values, - ) - .unwrap_or_else(|error| panic!("{error}")); + )?; - Self { + Ok(Self { num_objects, attribute_domains, frequency_tables, known_values, - } + }) } /// Returns the number of objects. @@ -261,9 +274,9 @@ impl ConsistencyOfDatabaseFrequencyTables { &self.known_values } - /// Returns the product of attribute domain sizes. - pub fn domain_size_product(&self) -> usize { - self.attribute_domains.iter().copied().product() + /// Largest attribute domain, or one for the empty attribute list. + pub fn max_domain_size(&self) -> usize { + self.attribute_domains.iter().copied().max().unwrap_or(1) } /// Returns the sum of all attribute-domain sizes. @@ -318,7 +331,7 @@ impl Problem for ConsistencyOfDatabaseFrequencyTables { ("num_objects", num_objects), ("num_attributes", num_attributes), ("total_domain_size", total_domain_size), - ("domain_size_product", domain_size_product), + ("max_domain_size", max_domain_size), ("num_frequency_tables", num_frequency_tables), ("num_frequency_cells", num_frequency_cells), ("num_known_values", num_known_values), @@ -386,17 +399,23 @@ impl Problem for ConsistencyOfDatabaseFrequencyTables { } impl crate::solvers::BruteForceProblem for ConsistencyOfDatabaseFrequencyTables { - fn dimensions(&self) -> Vec { - let mut dims = Vec::with_capacity(self.num_assignment_variables()); - for _ in 0..self.num_objects { - dims.extend(self.attribute_domains.iter().copied()); - } - dims + fn num_variables(&self) -> Result { + (self.num_objects) + .checked_mul(self.attribute_domains.len()) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a search coordinate size".into(), + ) + }) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.attribute_domains[variable % self.attribute_domains.len()]) } } crate::declare_variants! { - default ConsistencyOfDatabaseFrequencyTables => "domain_size_product^num_objects" create ConsistencyOfDatabaseFrequencyTablesCreateSpec, + default ConsistencyOfDatabaseFrequencyTables => "max_domain_size^(num_attributes * num_objects)" create ConsistencyOfDatabaseFrequencyTablesCreateSpec, } crate::register_brute_force! { @@ -407,19 +426,22 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "consistency_of_database_frequency_tables", - instance: Box::new(ConsistencyOfDatabaseFrequencyTables::new( - 6, - vec![2, 3, 2], - vec![ - FrequencyTable::new(0, 1, vec![vec![1, 1, 1], vec![1, 1, 1]]), - FrequencyTable::new(1, 2, vec![vec![1, 1], vec![0, 2], vec![1, 1]]), - ], - vec![ - KnownValue::new(0, 0, 0), - KnownValue::new(3, 0, 1), - KnownValue::new(1, 2, 1), - ], - )), + instance: Box::new( + ConsistencyOfDatabaseFrequencyTables::new( + 6, + vec![2, 3, 2], + vec![ + FrequencyTable::new(0, 1, vec![vec![1, 1, 1], vec![1, 1, 1]]), + FrequencyTable::new(1, 2, vec![vec![1, 1], vec![0, 2], vec![1, 1]]), + ], + vec![ + KnownValue::new(0, 0, 0), + KnownValue::new(3, 0, 1), + KnownValue::new(1, 2, 1), + ], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![ 0, 0, 0, 0, 1, 1, 0, 2, 1, 1, 0, 1, 1, 1, 1, 1, 2, 0 ]), diff --git a/src/models/misc/cosine_product_integration.rs b/src/models/misc/cosine_product_integration.rs index 824e46e09..e254caf93 100644 --- a/src/models/misc/cosine_product_integration.rs +++ b/src/models/misc/cosine_product_integration.rs @@ -49,28 +49,41 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // coefficients [2, 3, 5]: sign assignment (+2, +3, -5) = 0 -/// let problem = CosineProductIntegration::new(vec![2, 3, 5]); +/// let problem = CosineProductIntegration::new(vec![2, 3, 5]).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "CosineProductIntegrationData")] pub struct CosineProductIntegration { coefficients: Vec, } +#[derive(Deserialize)] +struct CosineProductIntegrationData { + coefficients: Vec, +} + +impl TryFrom for CosineProductIntegration { + type Error = crate::registry::ConstructionError; + + fn try_from(data: CosineProductIntegrationData) -> Result { + Self::new(data.coefficients) + } +} + impl CosineProductIntegration { /// Create a new CosineProductIntegration instance. /// - /// # Panics + /// # Errors /// - /// Panics if `coefficients` is empty. - pub fn new(coefficients: Vec) -> Self { - assert!( - !coefficients.is_empty(), - "CosineProductIntegration requires at least one coefficient" - ); - Self { coefficients } + /// Returns an error if `coefficients` is empty. + pub fn new(coefficients: Vec) -> Result { + if coefficients.is_empty() { + return Err("CosineProductIntegration requires at least one coefficient".into()); + } + Ok(Self { coefficients }) } /// Returns the cosine coefficients. @@ -132,8 +145,12 @@ impl Problem for CosineProductIntegration { } impl crate::solvers::BruteForceProblem for CosineProductIntegration { - fn dimensions(&self) -> Vec { - vec![2; self.num_coefficients()] + fn num_variables(&self) -> Result { + Ok(self.num_coefficients()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -149,7 +166,7 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "cosine_product_integration", - instance: Box::new(CosineProductIntegration::new(vec![2, 3, 5])), + instance: Box::new(CosineProductIntegration::new(vec![2, 3, 5]).unwrap()), optimal_config: serde_json::json!(vec![false, false, true]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/misc/cyclic_ordering.rs b/src/models/misc/cyclic_ordering.rs index 0edaffe5f..14d70565b 100644 --- a/src/models/misc/cyclic_ordering.rs +++ b/src/models/misc/cyclic_ordering.rs @@ -175,8 +175,12 @@ impl Problem for CyclicOrdering { } impl crate::solvers::BruteForceProblem for CyclicOrdering { - fn dimensions(&self) -> Vec { - vec![self.num_elements; self.num_elements] + fn num_variables(&self) -> Result { + Ok(self.num_elements) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_elements) } } diff --git a/src/models/misc/dynamic_storage_allocation.rs b/src/models/misc/dynamic_storage_allocation.rs index 37ccb3c60..81ff0e69d 100644 --- a/src/models/misc/dynamic_storage_allocation.rs +++ b/src/models/misc/dynamic_storage_allocation.rs @@ -172,11 +172,18 @@ impl Problem for DynamicStorageAllocation { } impl crate::solvers::BruteForceProblem for DynamicStorageAllocation { - fn dimensions(&self) -> Vec { - self.items - .iter() - .map(|&(_, _, s)| self.memory_size - s + 1) - .collect() + fn num_variables(&self) -> Result { + Ok(self.items.len()) + } + + fn dimension(&self, variable: usize) -> Result { + (self.memory_size - self.items[variable].2) + .checked_add(1usize) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a search coordinate size".into(), + ) + }) } } diff --git a/src/models/misc/ensemble_computation.rs b/src/models/misc/ensemble_computation.rs index abe93a81c..dd7bb7e15 100644 --- a/src/models/misc/ensemble_computation.rs +++ b/src/models/misc/ensemble_computation.rs @@ -228,8 +228,20 @@ impl Problem for EnsembleComputation { } impl crate::solvers::BruteForceProblem for EnsembleComputation { - fn dimensions(&self) -> Vec { - vec![self.universe_size + self.budget; 2 * self.budget] + fn num_variables(&self) -> Result { + (2usize).checked_mul(self.budget).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.universe_size) + .checked_add(self.budget) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a coordinate cardinality".into(), + ) + }) } } diff --git a/src/models/misc/expected_retrieval_cost.rs b/src/models/misc/expected_retrieval_cost.rs index a904aa39b..a86f3efcd 100644 --- a/src/models/misc/expected_retrieval_cost.rs +++ b/src/models/misc/expected_retrieval_cost.rs @@ -109,6 +109,15 @@ impl ExpectedRetrievalCost { Ok(Some(masses)) } + /// Number of intervening sectors, wrapping around the device. + pub(crate) fn latency_distance(&self, source: usize, target: usize) -> usize { + if source < target { + target - source - 1 + } else { + self.num_sectors - source + target - 1 + } + } + pub fn expected_cost( &self, config: &[usize], @@ -119,17 +128,7 @@ impl ExpectedRetrievalCost { let mut total = 0.0; for source in 0..self.num_sectors { for target in 0..self.num_sectors { - let latency = i64::try_from(latency_distance(self.num_sectors, source, target)) - .map_err(|_| { - crate::traits::EvaluationError::IntegerOverflow( - "converting expected-retrieval latency to i64".to_string(), - ) - })?; - let latency = crate::types::i64_to_exact_f64(latency).map_err(|_| { - crate::traits::EvaluationError::InexactFloatConversion( - "converting expected-retrieval latency to f64".to_string(), - ) - })?; + let latency = self.latency_distance(source, target) as f64; let term = masses[source] * masses[target] * latency; let next = total + term; if !term.is_finite() || !next.is_finite() { @@ -202,16 +201,12 @@ impl Problem for ExpectedRetrievalCost { } impl crate::solvers::BruteForceProblem for ExpectedRetrievalCost { - fn dimensions(&self) -> Vec { - vec![self.num_sectors; self.num_records()] + fn num_variables(&self) -> Result { + Ok(self.num_records()) } -} -fn latency_distance(num_sectors: usize, source: usize, target: usize) -> usize { - if source < target { - target - source - 1 - } else { - num_sectors - source + target - 1 + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_sectors) } } diff --git a/src/models/misc/factoring.rs b/src/models/misc/factoring.rs index eae8f35da..e943c45ba 100644 --- a/src/models/misc/factoring.rs +++ b/src/models/misc/factoring.rs @@ -238,8 +238,14 @@ impl Problem for Factoring { } impl crate::solvers::BruteForceProblem for Factoring { - fn dimensions(&self) -> Vec { - vec![2; self.m + self.n] + fn num_variables(&self) -> Result { + (self.m).checked_add(self.n).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/feasible_register_assignment.rs b/src/models/misc/feasible_register_assignment.rs index 2e485061c..7a1e0196c 100644 --- a/src/models/misc/feasible_register_assignment.rs +++ b/src/models/misc/feasible_register_assignment.rs @@ -53,7 +53,7 @@ inventory::submit! { /// vec![(0, 1), (0, 2), (1, 3)], /// 2, /// vec![0, 1, 0, 0], -/// ); +/// ).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); @@ -90,24 +90,22 @@ impl<'de> Deserialize<'de> for FeasibleRegisterAssignment { D: Deserializer<'de>, { let data = FeasibleRegisterAssignmentData::deserialize(deserializer)?; - let (dependencies, dependents) = Self::build_adjacency(data.num_vertices, &data.arcs); - Ok(Self { - num_vertices: data.num_vertices, - arcs: data.arcs, - num_registers: data.num_registers, - assignment: data.assignment, - dependencies, - dependents, - }) + Self::new( + data.num_vertices, + data.arcs, + data.num_registers, + data.assignment, + ) + .map_err(serde::de::Error::custom) } } impl FeasibleRegisterAssignment { /// Create a new Feasible Register Assignment instance. /// - /// # Panics + /// # Errors /// - /// Panics if any arc index is out of bounds (>= num_vertices), + /// Returns an error if any arc index is out of bounds (>= num_vertices), /// if any arc is a self-loop, if the assignment length does not /// match num_vertices, or if any assignment value >= num_registers. pub fn new( @@ -115,48 +113,48 @@ impl FeasibleRegisterAssignment { arcs: Vec<(usize, usize)>, num_registers: usize, assignment: Vec, - ) -> Self { + ) -> Result { for &(v, u) in &arcs { - assert!( - v < num_vertices && u < num_vertices, - "Arc ({}, {}) out of bounds for {} vertices", - v, - u, - num_vertices - ); - assert!(v != u, "Self-loop ({}, {}) not allowed in a DAG", v, u); + if !(v < num_vertices && u < num_vertices) { + return Err(format!( + "Arc ({}, {}) out of bounds for {} vertices", + v, u, num_vertices + ) + .into()); + }; + if v == u { + return Err(format!("Self-loop ({}, {}) not allowed in a DAG", v, u).into()); + }; } - assert_eq!( - assignment.len(), - num_vertices, - "Assignment length {} does not match num_vertices {}", - assignment.len(), - num_vertices - ); - if num_vertices > 0 { - assert!( - num_registers > 0, - "num_registers must be positive when there are vertices" - ); + if assignment.len() != num_vertices { + return Err(format!( + "Assignment length {} does not match num_vertices {}", + assignment.len(), + num_vertices + ) + .into()); + }; + if num_vertices > 0 && num_registers == 0 { + return Err("num_registers must be positive when there are vertices".into()); } for (v, &r) in assignment.iter().enumerate() { - assert!( - r < num_registers, - "Assignment[{}] = {} is out of bounds for {} registers", - v, - r, - num_registers - ); + if !(r < num_registers) { + return Err(format!( + "Assignment[{}] = {} is out of bounds for {} registers", + v, r, num_registers + ) + .into()); + }; } let (dependencies, dependents) = Self::build_adjacency(num_vertices, &arcs); - Self { + Ok(Self { num_vertices, arcs, num_registers, assignment, dependencies, dependents, - } + }) } /// Build dependency and dependent adjacency lists from arcs. @@ -303,8 +301,12 @@ impl Problem for FeasibleRegisterAssignment { } impl crate::solvers::BruteForceProblem for FeasibleRegisterAssignment { - fn dimensions(&self) -> Vec { - vec![self.num_vertices; self.num_vertices] + fn num_variables(&self) -> Result { + Ok(self.num_vertices) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_vertices) } } @@ -322,12 +324,10 @@ pub(crate) fn canonical_model_example_specs() -> Vec config [3, 1, 2, 0] - instance: Box::new(FeasibleRegisterAssignment::new( - 4, - vec![(0, 1), (0, 2), (1, 3)], - 2, - vec![0, 1, 0, 0], - )), + instance: Box::new( + FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]) + .unwrap(), + ), // config[v] = position: v0 at pos 3, v1 at pos 1, v2 at pos 2, v3 at pos 0 // Order: v3(pos0), v1(pos1), v2(pos2), v0(pos3) optimal_config: serde_json::json!(vec![3, 1, 2, 0]), diff --git a/src/models/misc/flow_shop_scheduling.rs b/src/models/misc/flow_shop_scheduling.rs index bd9ee27eb..b3a6fe844 100644 --- a/src/models/misc/flow_shop_scheduling.rs +++ b/src/models/misc/flow_shop_scheduling.rs @@ -35,7 +35,7 @@ inventory::submit! { /// /// # Representation /// -/// Configurations use Lehmer code encoding with `dims() = [n, n-1, ..., 1]`. +/// Configurations use Lehmer code encoding with `coordinate cardinalities = [n, n-1, ..., 1]`. /// A config `[c_0, c_1, ..., c_{n-1}]` where `c_i < n - i` is decoded by /// maintaining a list of available jobs and picking the `c_i`-th element: /// @@ -52,12 +52,13 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // 2 machines, 3 jobs, deadline 10 -/// let problem = FlowShopScheduling::new(2, vec![vec![2, 3], vec![3, 2], vec![1, 4]], 10); +/// let problem = FlowShopScheduling::new(2, vec![vec![2, 3], vec![3, 2], vec![1, 4]], 10).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "FlowShopSchedulingData")] pub struct FlowShopScheduling { /// Number of processors (machines). num_processors: usize, @@ -67,6 +68,21 @@ pub struct FlowShopScheduling { deadline: i64, } +#[derive(Deserialize)] +struct FlowShopSchedulingData { + num_processors: usize, + task_lengths: Vec>, + deadline: i64, +} + +impl TryFrom for FlowShopScheduling { + type Error = crate::registry::ConstructionError; + + fn try_from(data: FlowShopSchedulingData) -> Result { + Self::new(data.num_processors, data.task_lengths, data.deadline) + } +} + impl FlowShopScheduling { /// Create a new Flow Shop Scheduling instance. /// @@ -76,29 +92,35 @@ impl FlowShopScheduling { /// Each inner Vec must have length `num_processors`. /// * `deadline` - Global deadline D /// - /// # Panics - /// Panics if any job does not have exactly `num_processors` tasks. - pub fn new(num_processors: usize, task_lengths: Vec>, deadline: i64) -> Self { + /// # Errors + /// Returns an error if any job does not have exactly `num_processors` tasks. + pub fn new( + num_processors: usize, + task_lengths: Vec>, + deadline: i64, + ) -> Result { for (j, tasks) in task_lengths.iter().enumerate() { - assert_eq!( - tasks.len(), - num_processors, - "Job {} has {} tasks, expected {}", - j, - tasks.len(), - num_processors - ); + if tasks.len() != num_processors { + return Err(format!( + "Job {} has {} tasks, expected {}", + j, + tasks.len(), + num_processors + ) + .into()); + } } - assert!( - task_lengths.iter().flatten().all(|&length| length >= 0), - "task lengths must be nonnegative" - ); - assert!(deadline >= 0, "deadline must be nonnegative"); - Self { + if !(task_lengths.iter().flatten().all(|&length| length >= 0)) { + return Err("task lengths must be nonnegative".into()); + } + if !(deadline >= 0) { + return Err("deadline must be nonnegative".into()); + } + Ok(Self { num_processors, task_lengths, deadline, - } + }) } /// Get the number of processors. @@ -214,8 +236,12 @@ impl Problem for FlowShopScheduling { } impl crate::solvers::BruteForceProblem for FlowShopScheduling { - fn dimensions(&self) -> Vec { - super::lehmer_dims(self.num_jobs()) + fn num_variables(&self) -> Result { + Ok(self.num_jobs()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.num_jobs() - variable) } } @@ -231,17 +257,20 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "flow_shop_scheduling", - instance: Box::new(FlowShopScheduling::new( - 3, - vec![ - vec![3, 4, 2], - vec![2, 3, 5], - vec![4, 1, 3], - vec![1, 5, 4], - vec![3, 2, 3], - ], - 25, - )), + instance: Box::new( + FlowShopScheduling::new( + 3, + vec![ + vec![3, 4, 2], + vec![2, 3, 5], + vec![4, 1, 3], + vec![1, 5, 4], + vec![3, 2, 3], + ], + 25, + ) + .unwrap(), + ), // Job order [3,0,4,2,1] = Lehmer code [3,0,2,1,0], makespan 23 optimal_config: serde_json::json!(vec![3, 0, 4, 2, 1]), optimal_value: serde_json::json!(true), diff --git a/src/models/misc/grouping_by_swapping.rs b/src/models/misc/grouping_by_swapping.rs index 863c0fec6..3750fe0ef 100644 --- a/src/models/misc/grouping_by_swapping.rs +++ b/src/models/misc/grouping_by_swapping.rs @@ -27,12 +27,28 @@ inventory::submit! { /// adjacent swap position `i` (swap positions `i` and `i + 1`) or the special /// no-op value `string_len - 1`. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "GroupingBySwappingData")] pub struct GroupingBySwapping { alphabet_size: usize, string: Vec, budget: usize, } +#[derive(Deserialize)] +struct GroupingBySwappingData { + alphabet_size: usize, + string: Vec, + budget: usize, +} + +impl TryFrom for GroupingBySwapping { + type Error = crate::registry::ConstructionError; + + fn try_from(data: GroupingBySwappingData) -> Result { + Self::new(data.alphabet_size, data.string, data.budget) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct GroupingBySwappingCreateSpec { /// Optional alphabet size; omitted values are inferred from the string. @@ -61,55 +77,36 @@ impl TryFrom for GroupingBySwapping { .transpose()? .unwrap_or(0); let alphabet_size = spec.alphabet_size.unwrap_or(inferred_alphabet_size); - if alphabet_size < inferred_alphabet_size { - return Err(format!( - "alphabet size {alphabet_size} is smaller than inferred alphabet size {inferred_alphabet_size}" - ).into()); - } - if alphabet_size == 0 && !spec.string.is_empty() { - return Err("alphabet size must be positive for a non-empty string" - .to_string() - .into()); - } - if spec.string.is_empty() && spec.bound != 0 { - return Err("bound must be zero when the string is empty" - .to_string() - .into()); - } - - Ok(Self { - alphabet_size, - string: spec.string, - budget: spec.bound, - }) + Self::new(alphabet_size, spec.string, spec.bound) } } impl GroupingBySwapping { /// Create a new GroupingBySwapping instance. /// - /// # Panics + /// # Errors /// - /// Panics if the string contains a symbol outside the declared alphabet, + /// Returns an error if the string contains a symbol outside the declared alphabet, /// or if the string is empty while the budget is positive. - pub fn new(alphabet_size: usize, string: Vec, budget: usize) -> Self { - assert!( - alphabet_size > 0 || string.is_empty(), - "alphabet_size must be > 0 when string is non-empty" - ); - assert!( - string.iter().all(|&symbol| symbol < alphabet_size), - "input symbols must be less than alphabet_size" - ); - assert!( - !string.is_empty() || budget == 0, - "budget must be 0 when string is empty" - ); - Self { + pub fn new( + alphabet_size: usize, + string: Vec, + budget: usize, + ) -> Result { + if !(alphabet_size > 0 || string.is_empty()) { + return Err("alphabet_size must be > 0 when string is non-empty".into()); + } + if !(string.iter().all(|&symbol| symbol < alphabet_size)) { + return Err("input symbols must be less than alphabet_size".into()); + } + if !(!string.is_empty() || budget == 0) { + return Err("budget must be 0 when string is empty".into()); + } + Ok(Self { alphabet_size, string, budget, - } + }) } /// Returns the alphabet size. @@ -227,8 +224,12 @@ impl Problem for GroupingBySwapping { } impl crate::solvers::BruteForceProblem for GroupingBySwapping { - fn dimensions(&self) -> Vec { - vec![self.string_len(); self.budget] + fn num_variables(&self) -> Result { + Ok(self.budget) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.string_len()) } } @@ -244,7 +245,7 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "grouping_by_swapping", - instance: Box::new(GroupingBySwapping::new(3, vec![0, 1, 2, 0, 1, 2], 5)), + instance: Box::new(GroupingBySwapping::new(3, vec![0, 1, 2, 0, 1, 2], 5).unwrap()), optimal_config: serde_json::json!(vec![2, 1, 3, 5, 5]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/misc/integer_expression_membership.rs b/src/models/misc/integer_expression_membership.rs index e526e32b3..7bf5f5231 100644 --- a/src/models/misc/integer_expression_membership.rs +++ b/src/models/misc/integer_expression_membership.rs @@ -149,12 +149,13 @@ impl IntExpr { /// Box::new(IntExpr::Atom(5)), /// )), /// ); -/// let problem = IntegerExpressionMembership::new(expr, 12); +/// let problem = IntegerExpressionMembership::new(expr, 12).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "IntegerExpressionMembershipData")] pub struct IntegerExpressionMembership { /// The recursive expression tree. expression: IntExpr, @@ -162,19 +163,37 @@ pub struct IntegerExpressionMembership { target: i64, } +#[derive(Deserialize)] +struct IntegerExpressionMembershipData { + expression: IntExpr, + target: i64, +} + +impl TryFrom for IntegerExpressionMembership { + type Error = crate::registry::ConstructionError; + + fn try_from(data: IntegerExpressionMembershipData) -> Result { + Self::new(data.expression, data.target) + } +} + impl IntegerExpressionMembership { /// Create a new IntegerExpressionMembership instance. /// /// # Arguments /// * `expression` - The integer expression tree /// * `target` - The target integer K - pub fn new(expression: IntExpr, target: i64) -> Self { - assert!(target > 0, "target must be a positive integer (got 0)"); - assert!( - expression.all_atoms_positive(), - "all Atom values must be positive (> 0)" - ); - Self { expression, target } + pub fn new( + expression: IntExpr, + target: i64, + ) -> Result { + if target <= 0 { + return Err("target must be a positive integer (got 0)".into()); + } + if !(expression.all_atoms_positive()) { + return Err("all Atom values must be positive (> 0)".into()); + } + Ok(Self { expression, target }) } /// Returns a reference to the expression tree. @@ -245,8 +264,12 @@ impl Problem for IntegerExpressionMembership { } impl crate::solvers::BruteForceProblem for IntegerExpressionMembership { - fn dimensions(&self) -> Vec { - vec![2; self.num_union_nodes()] + fn num_variables(&self) -> Result { + Ok(self.num_union_nodes()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -281,7 +304,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec>, } +#[derive(Deserialize)] +struct JobShopSchedulingData { + num_processors: usize, + jobs: Vec>, +} + +impl TryFrom for JobShopScheduling { + type Error = crate::registry::ConstructionError; + + fn try_from(data: JobShopSchedulingData) -> Result { + Self::new(data.num_processors, data.jobs) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct JobShopSchedulingCreateSpec { /// Jobs expressed as ordered processor-duration operations. @@ -59,33 +74,7 @@ impl TryFrom for JobShopScheduling { "cannot infer processor count from an empty job list; provide num_processors" .to_string() })?; - if num_processors == 0 { - return Err("num_processors must be positive".to_string().into()); - } - - for (job_index, job) in spec.jobs.iter().enumerate() { - for (task_index, &(processor, _)) in job.iter().enumerate() { - if processor >= num_processors { - return Err(format!( - "job {job_index} task {task_index} uses processor {processor}, but num_processors is {num_processors}" - ).into()); - } - } - for (task_index, pair) in job.windows(2).enumerate() { - if pair[0].0 == pair[1].0 { - return Err(format!( - "job {job_index} tasks {task_index} and {} must use different processors", - task_index + 1 - ) - .into()); - } - } - } - - Ok(Self { - num_processors, - jobs: spec.jobs, - }) + Self::new(num_processors, spec.jobs) } } @@ -96,41 +85,39 @@ struct FlattenedTasks { } impl JobShopScheduling { - pub fn new(num_processors: usize, jobs: Vec>) -> Self { - let num_tasks: usize = jobs.iter().map(Vec::len).sum(); - if num_tasks > 0 { - assert!( - num_processors > 0, - "num_processors must be positive when tasks are present" - ); + pub fn new( + num_processors: usize, + jobs: Vec>, + ) -> Result { + if jobs.iter().any(|job| !job.is_empty()) && !(num_processors > 0) { + return Err("num_processors must be positive when tasks are present".into()); + } + if !(jobs.iter().flatten().all(|&(_, length)| length >= 0)) { + return Err("operation lengths must be nonnegative".into()); } - assert!( - jobs.iter().flatten().all(|&(_, length)| length >= 0), - "operation lengths must be nonnegative" - ); for (job_index, job) in jobs.iter().enumerate() { for (task_index, &(processor, _length)) in job.iter().enumerate() { - assert!( - processor < num_processors, - "job {job_index} task {task_index} uses processor {processor}, but num_processors = {num_processors}" - ); + if !(processor < num_processors) { + return Err(format!("job {job_index} task {task_index} uses processor {processor}, but num_processors = {num_processors}").into()); + } } for (task_index, pair) in job.windows(2).enumerate() { - assert_ne!( - pair[0].0, - pair[1].0, - "job {job_index} tasks {task_index} and {} must use different processors", - task_index + 1 - ); + if pair[0].0 == pair[1].0 { + return Err(format!( + "job {job_index} tasks {task_index} and {} must use different processors", + task_index + 1 + ) + .into()); + } } } - Self { + Ok(Self { num_processors, jobs, - } + }) } pub fn num_processors(&self) -> usize { @@ -317,12 +304,25 @@ impl Problem for JobShopScheduling { } impl crate::solvers::BruteForceProblem for JobShopScheduling { - fn dimensions(&self) -> Vec { - self.flatten_tasks() - .machine_task_ids - .into_iter() - .flat_map(|machine_tasks| super::lehmer_dims(machine_tasks.len())) - .collect() + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + let mut offset = variable; + for processor in 0..self.num_processors { + let count = self + .jobs + .iter() + .flatten() + .filter(|&&(machine, _)| machine == processor) + .count(); + if offset < count { + return Ok(count - offset); + } + offset -= count; + } + unreachable!("coordinate index is in range") } } @@ -338,16 +338,19 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "job_shop_scheduling", - instance: Box::new(JobShopScheduling::new( - 2, - vec![ - vec![(0, 3), (1, 4)], - vec![(1, 2), (0, 3), (1, 2)], - vec![(0, 4), (1, 3)], - vec![(1, 5), (0, 2)], - vec![(0, 2), (1, 3), (0, 1)], - ], - )), + instance: Box::new( + JobShopScheduling::new( + 2, + vec![ + vec![(0, 3), (1, 4)], + vec![(1, 2), (0, 3), (1, 2)], + vec![(0, 4), (1, 3)], + vec![(1, 5), (0, 2)], + vec![(0, 2), (1, 3), (0, 1)], + ], + ) + .unwrap(), + ), // Machine 0 order [0,3,5,8,9,11] => [0,0,0,0,0,0] // Machine 1 order [2,7,1,6,10,4] => [1,3,0,1,1,0] optimal_config: serde_json::json!(vec![0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0]), diff --git a/src/models/misc/knapsack.rs b/src/models/misc/knapsack.rs index 95d7fd023..b580f560e 100644 --- a/src/models/misc/knapsack.rs +++ b/src/models/misc/knapsack.rs @@ -38,21 +38,34 @@ inventory::submit! { /// use problemreductions::models::misc::Knapsack; /// use problemreductions::{Problem, BruteForce}; /// -/// let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); +/// let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "KnapsackData")] pub struct Knapsack { - #[serde(deserialize_with = "nonnegative_i64_vec::deserialize")] weights: Vec, - #[serde(deserialize_with = "nonnegative_i64_vec::deserialize")] values: Vec, - #[serde(deserialize_with = "nonnegative_i64::deserialize")] capacity: i64, } +#[derive(Deserialize)] +struct KnapsackData { + weights: Vec, + values: Vec, + capacity: i64, +} + +impl TryFrom for Knapsack { + type Error = crate::registry::ConstructionError; + + fn try_from(data: KnapsackData) -> Result { + Self::new(data.weights, data.values, data.capacity) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct KnapsackCreateSpec { /// Nonnegative item weights; defaults to one per value. @@ -67,47 +80,38 @@ impl TryFrom for Knapsack { fn try_from(spec: KnapsackCreateSpec) -> Result { let count = spec.values.len(); let weights = spec.weights.unwrap_or_else(|| vec![1; count]); - if weights.len() != count { - return Err("weights length must equal values length".to_string().into()); - } - if weights.iter().any(|&value| value < 0) - || spec.values.iter().any(|&value| value < 0) - || spec.capacity < 0 - { - return Err("weights, values, and capacity must be nonnegative" - .to_string() - .into()); - } - Ok(Self::new(weights, spec.values, spec.capacity)) + Self::new(weights, spec.values, spec.capacity) } } impl Knapsack { /// Create a new Knapsack instance. /// - /// # Panics - /// Panics if `weights` and `values` have different lengths, or if any + /// # Errors + /// Returns an error if `weights` and `values` have different lengths, or if any /// weight, value, or the capacity is negative. - pub fn new(weights: Vec, values: Vec, capacity: i64) -> Self { - assert_eq!( - weights.len(), - values.len(), - "weights and values must have the same length" - ); - assert!( - weights.iter().all(|&weight| weight >= 0), - "Knapsack weights must be nonnegative" - ); - assert!( - values.iter().all(|&value| value >= 0), - "Knapsack values must be nonnegative" - ); - assert!(capacity >= 0, "Knapsack capacity must be nonnegative"); - Self { + pub fn new( + weights: Vec, + values: Vec, + capacity: i64, + ) -> Result { + if weights.len() != values.len() { + return Err("weights and values must have the same length".into()); + } + if !(weights.iter().all(|&weight| weight >= 0)) { + return Err("Knapsack weights must be nonnegative".into()); + } + if !(values.iter().all(|&value| value >= 0)) { + return Err("Knapsack values must be nonnegative".into()); + } + if !(capacity >= 0) { + return Err("Knapsack capacity must be nonnegative".into()); + } + Ok(Self { weights, values, capacity, - } + }) } /// Returns the item weights. @@ -197,8 +201,12 @@ impl Problem for Knapsack { } impl crate::solvers::BruteForceProblem for Knapsack { - fn dimensions(&self) -> Vec { - vec![2; self.num_items()] + fn num_variables(&self) -> Result { + Ok(self.num_items()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -210,49 +218,13 @@ crate::register_brute_force! { Knapsack decode |_, indices: Vec| crate::config::config_to_bits(&indices), } -mod nonnegative_i64 { - use serde::de::Error; - use serde::{Deserialize, Deserializer}; - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = i64::deserialize(deserializer)?; - if value < 0 { - return Err(D::Error::custom(format!( - "expected nonnegative integer, got {value}" - ))); - } - Ok(value) - } -} - -mod nonnegative_i64_vec { - use serde::de::Error; - use serde::{Deserialize, Deserializer}; - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - let values = Vec::::deserialize(deserializer)?; - if let Some(value) = values.iter().copied().find(|value| *value < 0) { - return Err(D::Error::custom(format!( - "expected nonnegative integers, got {value}" - ))); - } - Ok(values) - } -} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { // 4 items: weights [2,3,4,5], values [3,4,5,7], capacity 7 // Optimal: items 0,3 → weight=7, value=10 vec![crate::example_db::specs::ModelExampleSpec { id: "knapsack", - instance: Box::new(Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7)), + instance: Box::new(Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7).unwrap()), optimal_config: serde_json::json!(vec![true, false, false, true]), optimal_value: serde_json::json!(10), }] diff --git a/src/models/misc/kth_largest_m_tuple.rs b/src/models/misc/kth_largest_m_tuple.rs index 8f6137c42..1bee02dae 100644 --- a/src/models/misc/kth_largest_m_tuple.rs +++ b/src/models/misc/kth_largest_m_tuple.rs @@ -141,12 +141,9 @@ impl KthLargestMTuple { self.sets.len() } - /// Returns the total number of m-tuples (product of set sizes). - pub fn total_tuples(&self) -> usize { - self.sets - .iter() - .try_fold(1usize, |total, set| total.checked_mul(set.len())) - .expect("KthLargestMTuple total tuple count exceeds usize") + /// Returns the total number of elements across the input sets. + pub fn num_elements(&self) -> usize { + self.sets.iter().map(Vec::len).sum() } fn has_at_least_k_qualifying_tuples(&self) -> Result { @@ -208,7 +205,7 @@ impl Problem for KthLargestMTuple { type Solution = (); type Value = Or; - crate::problem_parameters![("num_sets", num_sets), ("total_tuples", total_tuples),]; + crate::problem_parameters![("num_sets", num_sets), ("num_elements", num_elements),]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -220,15 +217,19 @@ impl Problem for KthLargestMTuple { } impl crate::solvers::BruteForceProblem for KthLargestMTuple { - fn dimensions(&self) -> Vec { - vec![] + fn num_variables(&self) -> Result { + Ok(0) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(0) } } -// Best known: brute-force enumeration of all tuples, O(total_tuples * num_sets). +// Best known: brute-force enumeration of all tuples, O(product_i |X_i| * num_sets), bounded by AM-GM. // No sub-exponential exact algorithm is known for the general case. crate::declare_variants! { - default KthLargestMTuple => "total_tuples * num_sets" create KthLargestMTupleCreateSpec, + default KthLargestMTuple => "(num_elements / num_sets)^num_sets * num_sets" create KthLargestMTupleCreateSpec, } crate::register_brute_force! { diff --git a/src/models/misc/longest_common_subsequence.rs b/src/models/misc/longest_common_subsequence.rs index 15bb0c0d2..fb0cd0f44 100644 --- a/src/models/misc/longest_common_subsequence.rs +++ b/src/models/misc/longest_common_subsequence.rs @@ -36,12 +36,27 @@ inventory::submit! { /// subsequence consists of the symbols before padding starts. The objective is /// to maximize the effective length. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "LongestCommonSubsequenceData")] pub struct LongestCommonSubsequence { alphabet_size: usize, strings: Vec>, max_length: usize, } +#[derive(Deserialize)] +struct LongestCommonSubsequenceData { + alphabet_size: usize, + strings: Vec>, +} + +impl TryFrom for LongestCommonSubsequence { + type Error = crate::registry::ConstructionError; + + fn try_from(data: LongestCommonSubsequenceData) -> Result { + Self::new(data.alphabet_size, data.strings) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct LongestCommonSubsequenceCreateSpec { /// Optional alphabet size; omitted values are inferred from the strings. @@ -55,11 +70,6 @@ impl TryFrom for LongestCommonSubsequence { type Error = crate::registry::ConstructionError; fn try_from(spec: LongestCommonSubsequenceCreateSpec) -> Result { - if !spec.strings.iter().any(|string| !string.is_empty()) { - return Err("at least one input string must be non-empty" - .to_string() - .into()); - } let inferred_alphabet_size = spec .strings .iter() @@ -74,21 +84,7 @@ impl TryFrom for LongestCommonSubsequence { .transpose()? .unwrap_or(0); let alphabet_size = spec.alphabet_size.unwrap_or(inferred_alphabet_size); - if alphabet_size < inferred_alphabet_size { - return Err(format!( - "alphabet size {alphabet_size} is smaller than inferred alphabet size {inferred_alphabet_size}" - ).into()); - } - if alphabet_size == 0 { - return Err("alphabet size must be positive".to_string().into()); - } - let max_length = spec.strings.iter().map(Vec::len).min().unwrap_or(0); - - Ok(Self { - alphabet_size, - strings: spec.strings, - max_length, - }) + Self::new(alphabet_size, spec.strings) } } @@ -98,29 +94,31 @@ impl LongestCommonSubsequence { /// The `max_length` is computed automatically as the minimum of all string /// lengths (the maximum possible common subsequence length). /// - /// # Panics + /// # Errors /// - /// Panics if `alphabet_size == 0` and any input string is non-empty, or if + /// Returns an error if `alphabet_size == 0` and any input string is non-empty, or if /// an input symbol is outside the declared alphabet, or if all strings are /// empty (max_length would be 0, requiring at least one non-empty string). - pub fn new(alphabet_size: usize, strings: Vec>) -> Self { + pub fn new( + alphabet_size: usize, + strings: Vec>, + ) -> Result { let max_length = strings.iter().map(|s| s.len()).min().unwrap_or(0); - assert!( - alphabet_size > 0 || strings.iter().all(|s| s.is_empty()), - "alphabet_size must be > 0 when any input string is non-empty" - ); - assert!( - strings - .iter() - .flat_map(|s| s.iter()) - .all(|&symbol| symbol < alphabet_size), - "input symbols must be less than alphabet_size" - ); - Self { + if !(alphabet_size > 0 || strings.iter().all(|s| s.is_empty())) { + return Err("alphabet_size must be > 0 when any input string is non-empty".into()); + } + if !(strings + .iter() + .flat_map(|s| s.iter()) + .all(|&symbol| symbol < alphabet_size)) + { + return Err("input symbols must be less than alphabet_size".into()); + } + Ok(Self { alphabet_size, strings, max_length, - } + }) } /// Returns the alphabet size. @@ -274,8 +272,14 @@ impl Problem for LongestCommonSubsequence { } impl crate::solvers::BruteForceProblem for LongestCommonSubsequence { - fn dimensions(&self) -> Vec { - vec![self.alphabet_size + 1; self.max_length] + fn num_variables(&self) -> Result { + Ok(self.max_length) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.alphabet_size).checked_add(1usize).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } @@ -291,17 +295,20 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "longest_common_subsequence", - instance: Box::new(LongestCommonSubsequence::new( - 2, - vec![ - vec![0, 1, 0, 1, 1, 0], - vec![1, 0, 0, 1, 0, 1], - vec![0, 0, 1, 0, 1, 1], - vec![1, 1, 0, 0, 1, 0], - vec![0, 1, 0, 1, 0, 1], - vec![1, 0, 1, 0, 1, 0], - ], - )), + instance: Box::new( + LongestCommonSubsequence::new( + 2, + vec![ + vec![0, 1, 0, 1, 1, 0], + vec![1, 0, 0, 1, 0, 1], + vec![0, 0, 1, 0, 1, 1], + vec![1, 1, 0, 0, 1, 0], + vec![0, 1, 0, 1, 0, 1], + vec![1, 0, 1, 0, 1, 0], + ], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![Some(0), Some(0), Some(1), Some(0), None, None]), optimal_value: serde_json::json!(4), }] diff --git a/src/models/misc/maximum_likelihood_ranking.rs b/src/models/misc/maximum_likelihood_ranking.rs index 6483aa1a8..dce9ee679 100644 --- a/src/models/misc/maximum_likelihood_ranking.rs +++ b/src/models/misc/maximum_likelihood_ranking.rs @@ -49,55 +49,71 @@ inventory::submit! { /// vec![2, 1, 0, 4], /// vec![0, 2, 1, 0], /// ]; -/// let problem = MaximumLikelihoodRanking::new(matrix); +/// let problem = MaximumLikelihoodRanking::new(matrix).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MaximumLikelihoodRankingData")] pub struct MaximumLikelihoodRanking { matrix: Vec>, } +#[derive(Deserialize)] +struct MaximumLikelihoodRankingData { + matrix: Vec>, +} + +impl TryFrom for MaximumLikelihoodRanking { + type Error = crate::registry::ConstructionError; + fn try_from(data: MaximumLikelihoodRankingData) -> Result { + Self::new(data.matrix) + } +} + impl MaximumLikelihoodRanking { /// Create a new MaximumLikelihoodRanking instance. /// - /// # Panics - /// Panics if the matrix is not square, if any diagonal element is nonzero, + /// # Errors + /// Returns an error if the matrix is not square, if any diagonal element is nonzero, /// or if the pairwise sums `a_ij + a_ji` are not the same constant for /// all `i != j`. - pub fn new(matrix: Vec>) -> Self { + pub fn new(matrix: Vec>) -> Result { let n = matrix.len(); for (i, row) in matrix.iter().enumerate() { - assert_eq!( - row.len(), - n, - "matrix must be square: row {i} has length {} but expected {n}", - row.len() - ); - assert_eq!( - row[i], 0, - "diagonal entries must be zero: matrix[{i}][{i}] = {}", - row[i] - ); + if row.len() != n { + return Err(format!( + "matrix must be square: row {i} has length {} but expected {n}", + row.len() + ) + .into()); + } + if row[i] != 0 { + return Err(format!( + "diagonal entries must be zero: matrix[{i}][{i}] = {}", + row[i] + ) + .into()); + } } let mut comparison_count = None; for (i, row) in matrix.iter().enumerate() { for (j, &entry) in row.iter().enumerate().skip(i + 1) { - let pair_sum = entry + matrix[j][i]; + let pair_sum = i128::from(entry) + i128::from(matrix[j][i]); match comparison_count { None => comparison_count = Some(pair_sum), - Some(expected) => assert_eq!( - pair_sum, - expected, - "all off-diagonal pairs must have the same comparison count: matrix[{i}][{j}] + matrix[{j}][{i}] = {pair_sum}, expected {expected}" - ), + Some(expected) => { + if pair_sum != expected { + return Err(format!("all off-diagonal pairs must have the same comparison count: matrix[{i}][{j}] + matrix[{j}][{i}] = {pair_sum}, expected {expected}").into()); + } + } } } } - Self { matrix } + Ok(Self { matrix }) } /// Returns the comparison matrix. @@ -182,9 +198,12 @@ impl Problem for MaximumLikelihoodRanking { } impl crate::solvers::BruteForceProblem for MaximumLikelihoodRanking { - fn dimensions(&self) -> Vec { - let n = self.num_items(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.num_items()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_items()) } } @@ -212,7 +231,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec, usize)>, } +#[derive(Deserialize)] +struct MinimumAxiomSetData { + num_sentences: usize, + true_sentences: Vec, + implications: Vec<(Vec, usize)>, +} + +impl TryFrom for MinimumAxiomSet { + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumAxiomSetData) -> Result { + Self::new(data.num_sentences, data.true_sentences, data.implications) + } +} + impl MinimumAxiomSet { /// Create a new Minimum Axiom Set instance. /// - /// # Panics + /// # Errors /// - /// Panics if any true sentence index is out of range, + /// Returns an error if any true sentence index is out of range, /// if true sentences contain duplicates, /// or if any implication references a sentence outside S. pub fn new( num_sentences: usize, true_sentences: Vec, implications: Vec<(Vec, usize)>, - ) -> Self { + ) -> Result { // Validate true sentences for &s in &true_sentences { - assert!( - s < num_sentences, - "True sentence index {s} out of range [0, {num_sentences})" - ); + if !(s < num_sentences) { + return Err( + format!("True sentence index {s} out of range [0, {num_sentences})").into(), + ); + } } // Check no duplicates let mut seen = vec![false; num_sentences]; for &s in &true_sentences { - assert!(!seen[s], "Duplicate true sentence index {s}"); + if !(!seen[s]) { + return Err(format!("Duplicate true sentence index {s}").into()); + } seen[s] = true; } // Validate implications for (antecedents, consequent) in &implications { for &a in antecedents { - assert!( - a < num_sentences, - "Implication antecedent {a} out of range [0, {num_sentences})" - ); + if !(a < num_sentences) { + return Err(format!( + "Implication antecedent {a} out of range [0, {num_sentences})" + ) + .into()); + } + } + if !(*consequent < num_sentences) { + return Err(format!( + "Implication consequent {consequent} out of range [0, {num_sentences})" + ) + .into()); } - assert!( - *consequent < num_sentences, - "Implication consequent {consequent} out of range [0, {num_sentences})" - ); } - Self { + Ok(Self { num_sentences, true_sentences, implications, - } + }) } /// Returns the total number of sentences |S|. @@ -217,8 +239,12 @@ impl Problem for MinimumAxiomSet { } impl crate::solvers::BruteForceProblem for MinimumAxiomSet { - fn dimensions(&self) -> Vec { - vec![2; self.num_true_sentences()] + fn num_variables(&self) -> Result { + Ok(self.num_true_sentences()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -236,20 +262,23 @@ pub(crate) fn canonical_model_example_specs() -> Vec, + num_leaves: usize, +} + +impl TryFrom for MinimumCodeGenerationOneRegister { + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumCodeGenerationOneRegisterData) -> Result { + Self::new(data.num_vertices, data.edges, data.num_leaves) + } +} + impl MinimumCodeGenerationOneRegister { /// Create a new instance. /// @@ -75,41 +90,51 @@ impl MinimumCodeGenerationOneRegister { /// * `edges` - Directed arcs (parent, child); parent depends on child /// * `num_leaves` - Number of leaf vertices (out-degree 0) /// - /// # Panics + /// # Errors /// - /// Panics if any edge index is out of bounds, if any vertex has + /// Returns an error if any edge index is out of bounds, if any vertex has /// out-degree > 2, or if `num_leaves > num_vertices`. - pub fn new(num_vertices: usize, edges: Vec<(usize, usize)>, num_leaves: usize) -> Self { - assert!( - num_leaves <= num_vertices, - "num_leaves ({num_leaves}) exceeds num_vertices ({num_vertices})" - ); + pub fn new( + num_vertices: usize, + edges: Vec<(usize, usize)>, + num_leaves: usize, + ) -> Result { + if !(num_leaves <= num_vertices) { + return Err( + format!("num_leaves ({num_leaves}) exceeds num_vertices ({num_vertices})").into(), + ); + } let mut out_degree = vec![0usize; num_vertices]; for &(parent, child) in &edges { - assert!( - parent < num_vertices && child < num_vertices, - "Edge ({parent}, {child}) out of bounds for {num_vertices} vertices" - ); - assert!( - parent != child, - "Self-loop ({parent}, {parent}) not allowed" - ); + if !(parent < num_vertices && child < num_vertices) { + return Err(format!( + "Edge ({parent}, {child}) out of bounds for {num_vertices} vertices" + ) + .into()); + } + if parent == child { + return Err(format!("Self-loop ({parent}, {parent}) not allowed").into()); + } out_degree[parent] += 1; } for (v, °) in out_degree.iter().enumerate() { - assert!(deg <= 2, "Vertex {v} has out-degree {deg} > 2"); + if !(deg <= 2) { + return Err(format!("Vertex {v} has out-degree {deg} > 2").into()); + } } // Verify leaf count: leaves are vertices with out-degree 0 let actual_leaves = out_degree.iter().filter(|&&d| d == 0).count(); - assert_eq!( - actual_leaves, num_leaves, - "Declared num_leaves ({num_leaves}) != actual leaf count ({actual_leaves})" - ); - Self { + if actual_leaves != num_leaves { + return Err(format!( + "Declared num_leaves ({num_leaves}) != actual leaf count ({actual_leaves})" + ) + .into()); + } + Ok(Self { num_vertices, edges, num_leaves, - } + }) } /// Get the number of vertices. @@ -338,9 +363,12 @@ impl Problem for MinimumCodeGenerationOneRegister { } impl crate::solvers::BruteForceProblem for MinimumCodeGenerationOneRegister { - fn dimensions(&self) -> Vec { - let n_internal = self.num_internal(); - vec![n_internal; n_internal] + fn num_variables(&self) -> Result { + Ok(self.num_internal()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_internal()) } } @@ -364,20 +392,23 @@ pub(crate) fn canonical_model_example_specs() -> Vec pos 3, idx 1=v1 -> pos 2, ...) - instance: Box::new(MinimumCodeGenerationOneRegister::new( - 7, - vec![ - (0, 1), - (0, 2), - (1, 3), - (1, 4), - (2, 3), - (2, 5), - (3, 5), - (3, 6), - ], - 3, - )), + instance: Box::new( + MinimumCodeGenerationOneRegister::new( + 7, + vec![ + (0, 1), + (0, 2), + (1, 3), + (1, 4), + (2, 3), + (2, 5), + (3, 5), + (3, 6), + ], + 3, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![3, 2, 1, 0]), optimal_value: serde_json::json!(8), }] diff --git a/src/models/misc/minimum_code_generation_parallel_assignments.rs b/src/models/misc/minimum_code_generation_parallel_assignments.rs index 405f7a962..c69ba9c11 100644 --- a/src/models/misc/minimum_code_generation_parallel_assignments.rs +++ b/src/models/misc/minimum_code_generation_parallel_assignments.rs @@ -50,39 +50,62 @@ inventory::submit! { /// (2, vec![3]), /// (3, vec![1, 2]), /// ]; -/// let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments); +/// let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumCodeGenerationParallelAssignmentsData")] pub struct MinimumCodeGenerationParallelAssignments { num_variables: usize, assignments: Vec<(usize, Vec)>, } +#[derive(Deserialize)] +struct MinimumCodeGenerationParallelAssignmentsData { + num_variables: usize, + assignments: Vec<(usize, Vec)>, +} + +impl TryFrom + for MinimumCodeGenerationParallelAssignments +{ + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumCodeGenerationParallelAssignmentsData) -> Result { + Self::new(data.num_variables, data.assignments) + } +} + impl MinimumCodeGenerationParallelAssignments { /// Create a new MinimumCodeGenerationParallelAssignments instance. /// - /// # Panics - /// Panics if any target variable or read variable index is >= num_variables. - pub fn new(num_variables: usize, assignments: Vec<(usize, Vec)>) -> Self { + /// # Errors + /// Returns an error if any target variable or read variable index is >= num_variables. + pub fn new( + num_variables: usize, + assignments: Vec<(usize, Vec)>, + ) -> Result { for (i, (target, reads)) in assignments.iter().enumerate() { - assert!( - *target < num_variables, - "assignment {i}: target variable {target} >= num_variables {num_variables}" - ); + if !(*target < num_variables) { + return Err(format!( + "assignment {i}: target variable {target} >= num_variables {num_variables}" + ) + .into()); + } for &r in reads { - assert!( - r < num_variables, - "assignment {i}: read variable {r} >= num_variables {num_variables}" - ); + if !(r < num_variables) { + return Err(format!( + "assignment {i}: read variable {r} >= num_variables {num_variables}" + ) + .into()); + } } } - Self { + Ok(Self { num_variables, assignments, - } + }) } /// Returns the number of variables. @@ -175,9 +198,12 @@ impl Problem for MinimumCodeGenerationParallelAssignments { } impl crate::solvers::BruteForceProblem for MinimumCodeGenerationParallelAssignments { - fn dimensions(&self) -> Vec { - let m = self.num_assignments(); - vec![m; m] + fn num_variables(&self) -> Result { + Ok(self.num_assignments()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_assignments()) } } @@ -206,10 +232,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec, } +#[derive(Deserialize)] +struct MinimumCodeGenerationUnlimitedRegistersData { + num_vertices: usize, + left_arcs: Vec<(usize, usize)>, + right_arcs: Vec<(usize, usize)>, +} + +impl TryFrom + for MinimumCodeGenerationUnlimitedRegisters +{ + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumCodeGenerationUnlimitedRegistersData) -> Result { + Self::new(data.num_vertices, data.left_arcs, data.right_arcs) + } +} + impl MinimumCodeGenerationUnlimitedRegisters { /// Create a new instance. /// @@ -80,66 +97,69 @@ impl MinimumCodeGenerationUnlimitedRegisters { /// * `left_arcs` - Left operand arcs (parent, child); child register is destroyed by OP /// * `right_arcs` - Right operand arcs (parent, child); child register is preserved /// - /// # Panics + /// # Errors /// - /// Panics if any arc index is out of bounds, if any vertex has out-degree > 2, + /// Returns an error if any arc index is out of bounds, if any vertex has out-degree > 2, /// if left and right arcs for binary vertices are inconsistent, or if a vertex /// has a self-loop. pub fn new( num_vertices: usize, left_arcs: Vec<(usize, usize)>, right_arcs: Vec<(usize, usize)>, - ) -> Self { + ) -> Result { let mut left_count = vec![0usize; num_vertices]; let mut right_count = vec![0usize; num_vertices]; for &(parent, child) in &left_arcs { - assert!( - parent < num_vertices && child < num_vertices, - "Left arc ({parent}, {child}) out of bounds for {num_vertices} vertices" - ); - assert!( - parent != child, - "Self-loop ({parent}, {parent}) not allowed" - ); + if !(parent < num_vertices && child < num_vertices) { + return Err(format!( + "Left arc ({parent}, {child}) out of bounds for {num_vertices} vertices" + ) + .into()); + } + if parent == child { + return Err(format!("Self-loop ({parent}, {parent}) not allowed").into()); + } left_count[parent] += 1; } for &(parent, child) in &right_arcs { - assert!( - parent < num_vertices && child < num_vertices, - "Right arc ({parent}, {child}) out of bounds for {num_vertices} vertices" - ); - assert!( - parent != child, - "Self-loop ({parent}, {parent}) not allowed" - ); + if !(parent < num_vertices && child < num_vertices) { + return Err(format!( + "Right arc ({parent}, {child}) out of bounds for {num_vertices} vertices" + ) + .into()); + } + if parent == child { + return Err(format!("Self-loop ({parent}, {parent}) not allowed").into()); + } right_count[parent] += 1; } for v in 0..num_vertices { let out = left_count[v] + right_count[v]; - assert!(out <= 2, "Vertex {v} has out-degree {out} > 2"); + if !(out <= 2) { + return Err(format!("Vertex {v} has out-degree {out} > 2").into()); + } // Binary vertex: exactly one left and one right - if out == 2 { - assert!( - left_count[v] == 1 && right_count[v] == 1, - "Binary vertex {v} must have exactly 1 left and 1 right arc" + if out == 2 && !(left_count[v] == 1 && right_count[v] == 1) { + return Err( + format!("Binary vertex {v} must have exactly 1 left and 1 right arc").into(), ); } // Unary vertex: one left arc (result overwrites operand register) - if out == 1 { - assert!( - left_count[v] == 1 && right_count[v] == 0, + if out == 1 && !(left_count[v] == 1 && right_count[v] == 0) { + return Err(format!( "Unary vertex {v} must have exactly 1 left arc and 0 right arcs" - ); + ) + .into()); } } - Self { + Ok(Self { num_vertices, left_arcs, right_arcs, - } + }) } /// Get the number of vertices. @@ -364,9 +384,12 @@ impl Problem for MinimumCodeGenerationUnlimitedRegisters { } impl crate::solvers::BruteForceProblem for MinimumCodeGenerationUnlimitedRegisters { - fn dimensions(&self) -> Vec { - let n_internal = self.num_internal(); - vec![n_internal; n_internal] + fn num_variables(&self) -> Result { + Ok(self.num_internal()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_internal()) } } @@ -389,11 +412,14 @@ pub(crate) fn canonical_model_example_specs() -> Vec>, @@ -73,78 +74,52 @@ struct MinimumDecisionTreeCreateSpec { impl TryFrom for MinimumDecisionTree { type Error = crate::registry::ConstructionError; fn try_from(spec: MinimumDecisionTreeCreateSpec) -> Result { - if spec.num_objects < 2 { - return Err("num_objects must be at least 2".into()); - } - if spec.num_tests == 0 { - return Err("num_tests must be positive".into()); - } - if spec.test_matrix.len() != spec.num_tests { - return Err("test_matrix row count must equal num_tests".into()); - } - if spec - .test_matrix - .iter() - .any(|row| row.len() != spec.num_objects) - { - return Err("each test_matrix row must have num_objects columns".into()); - } - for a in 0..spec.num_objects { - for b in a + 1..spec.num_objects { - if !(0..spec.num_tests) - .any(|test| spec.test_matrix[test][a] != spec.test_matrix[test][b]) - { - return Err( - format!("objects {a} and {b} are not distinguished by any test").into(), - ); - } - } - } - Ok(Self { - test_matrix: spec.test_matrix, - num_objects: spec.num_objects, - num_tests: spec.num_tests, - }) + Self::new(spec.test_matrix, spec.num_objects, spec.num_tests) } } impl MinimumDecisionTree { /// Create a new MinimumDecisionTree problem. /// - /// # Panics + /// # Errors /// - If num_objects < 2 or num_tests < 1 /// - If test_matrix dimensions don't match /// - If tests don't distinguish all object pairs - pub fn new(test_matrix: Vec>, num_objects: usize, num_tests: usize) -> Self { - assert!(num_objects >= 2, "Need at least 2 objects"); - assert!(num_tests >= 1, "Need at least 1 test"); - assert_eq!( - test_matrix.len(), - num_tests, - "test_matrix must have num_tests rows" - ); + pub fn new( + test_matrix: Vec>, + num_objects: usize, + num_tests: usize, + ) -> Result { + if !(num_objects >= 2) { + return Err("Need at least 2 objects".into()); + } + if num_tests == 0 { + return Err("Need at least 1 test".into()); + } + if test_matrix.len() != num_tests { + return Err("test_matrix must have num_tests rows".into()); + } for (j, row) in test_matrix.iter().enumerate() { - assert_eq!( - row.len(), - num_objects, - "test_matrix[{j}] must have num_objects columns" - ); + if row.len() != num_objects { + return Err(format!("test_matrix[{j}] must have num_objects columns").into()); + } } // Check that every pair of objects is distinguished by at least one test for a in 0..num_objects { for b in (a + 1)..num_objects { let distinguished = (0..num_tests).any(|j| test_matrix[j][a] != test_matrix[j][b]); - assert!( - distinguished, - "Objects {a} and {b} are not distinguished by any test" - ); + if !(distinguished) { + return Err( + format!("Objects {a} and {b} are not distinguished by any test").into(), + ); + } } } - Self { + Ok(Self { test_matrix, num_objects, num_tests, - } + }) } /// Get the number of objects. @@ -163,8 +138,17 @@ impl MinimumDecisionTree { } /// Number of internal node slots in the flattened complete binary tree. - fn num_tree_slots(&self) -> usize { - (1usize << (self.num_objects - 1)) - 1 + fn num_tree_slots(&self) -> Result { + self.num_objects + .checked_sub(1) + .and_then(|depth| u32::try_from(depth).ok()) + .and_then(|depth| 1usize.checked_shl(depth)) + .map(|leaves| leaves - 1) + .ok_or_else(|| { + crate::traits::EvaluationError::IntegerOverflow( + "representing the decision-tree witness slots".into(), + ) + }) } /// Sentinel value meaning "this node is a leaf". @@ -176,7 +160,7 @@ impl MinimumDecisionTree { /// or None if the tree is invalid (doesn't identify all objects uniquely). fn simulate(&self, config: &[usize]) -> Result, crate::traits::EvaluationError> { let sentinel = self.leaf_sentinel(); - let max_slots = self.num_tree_slots(); + let max_slots = self.num_tree_slots()?; let mut seen_leaves = std::collections::HashSet::new(); let mut total_depth = 0_i64; @@ -232,7 +216,7 @@ impl Problem for MinimumDecisionTree { config: &Self::Solution, ) -> Result, crate::traits::EvaluationError> { Ok({ - if config.len() != self.num_tree_slots() { + if config.len() != self.num_tree_slots()? { return Err(crate::traits::EvaluationError::InvalidConfiguration( "decision-tree encoding length does not match the instance".into(), )); @@ -247,9 +231,14 @@ impl Problem for MinimumDecisionTree { } impl crate::solvers::BruteForceProblem for MinimumDecisionTree { - fn dimensions(&self) -> Vec { - // Each internal node can hold test 0..num_tests-1 or sentinel (leaf) - vec![self.num_tests + 1; self.num_tree_slots()] + fn num_variables(&self) -> Result { + Ok(self.num_tree_slots()?) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.num_tests).checked_add(1usize).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } @@ -265,15 +254,18 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "minimum_decision_tree", - instance: Box::new(MinimumDecisionTree::new( - vec![ - vec![true, true, false, false], - vec![true, false, false, false], - vec![false, true, false, true], - ], - 4, - 3, - )), + instance: Box::new( + MinimumDecisionTree::new( + vec![ + vec![true, true, false, false], + vec![true, false, false, false], + vec![false, true, false, true], + ], + 4, + 3, + ) + .unwrap(), + ), // T0 at root, T2 left, T1 right, rest are leaves (sentinel=3) optimal_config: serde_json::json!(vec![0, 2, 1, 3, 3, 3, 3]), optimal_value: serde_json::json!(8), diff --git a/src/models/misc/minimum_discrete_planar_inverse_kinematics.rs b/src/models/misc/minimum_discrete_planar_inverse_kinematics.rs index f002f1bc5..c6ab52e24 100644 --- a/src/models/misc/minimum_discrete_planar_inverse_kinematics.rs +++ b/src/models/misc/minimum_discrete_planar_inverse_kinematics.rs @@ -113,16 +113,12 @@ impl MinimumDiscretePlanarInverseKinematics { if orientation_samples.len() != n { return Err("orientation_samples must have one entry per link".into()); } - let mut total_configurations = 1_usize; for (link, samples) in orientation_samples.iter().enumerate() { if samples.is_empty() { return Err( format!("link {link} must have at least one candidate orientation").into(), ); } - total_configurations = total_configurations - .checked_mul(samples.len()) - .ok_or("orientation configuration count exceeds usize")?; for (sample, &angle) in samples.iter().enumerate() { if !angle.is_finite() { return Err(format!( @@ -180,16 +176,6 @@ impl MinimumDiscretePlanarInverseKinematics { self.link_lengths.len() } - /// Total number of configurations (product of per-link sample counts): - /// `prod_{j=1}^n m_j`. This is the size of the brute-force search space. - pub fn total_configurations(&self) -> usize { - self.orientation_samples - .iter() - .map(|samples| samples.len()) - .try_fold(1_usize, usize::checked_mul) - .expect("validated orientation configuration count must fit usize") - } - /// Total number of sampled orientations across all links: /// `sum_{j=1}^n m_j`. This is the QUBO variable count for the one-hot /// encoding used by the QUBO reduction. @@ -276,7 +262,6 @@ impl Problem for MinimumDiscretePlanarInverseKinematics { type Value = Min; crate::problem_parameters![ - ("total_configurations", total_configurations), ("num_links", num_links), ("num_orientation_samples", num_orientation_samples), ]; @@ -313,16 +298,17 @@ impl Problem for MinimumDiscretePlanarInverseKinematics { } impl crate::solvers::BruteForceProblem for MinimumDiscretePlanarInverseKinematics { - fn dimensions(&self) -> Vec { - self.orientation_samples - .iter() - .map(|samples| samples.len()) - .collect() + fn num_variables(&self) -> Result { + Ok(self.orientation_samples.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.orientation_samples[variable].len()) } } crate::declare_variants! { - default MinimumDiscretePlanarInverseKinematics => "total_configurations", + default MinimumDiscretePlanarInverseKinematics => "(num_orientation_samples / num_links)^num_links", } crate::register_brute_force! { diff --git a/src/models/misc/minimum_disjunctive_normal_form.rs b/src/models/misc/minimum_disjunctive_normal_form.rs index ac2b429c0..638f83e45 100644 --- a/src/models/misc/minimum_disjunctive_normal_form.rs +++ b/src/models/misc/minimum_disjunctive_normal_form.rs @@ -65,11 +65,12 @@ impl PrimeImplicant { /// /// // f(x1,x2,x3) = 1 when exactly 1 or 2 variables are true /// let truth_table = vec![false, true, true, true, true, true, true, false]; -/// let problem = MinimumDisjunctiveNormalForm::new(3, truth_table); +/// let problem = MinimumDisjunctiveNormalForm::new(3, truth_table).unwrap(); /// let solver = BruteForce::new(); /// let value = solver.solve(&problem).unwrap(); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumDisjunctiveNormalFormData")] pub struct MinimumDisjunctiveNormalForm { /// Number of Boolean variables. num_variables: usize, @@ -81,38 +82,59 @@ pub struct MinimumDisjunctiveNormalForm { minterms: Vec, } +#[derive(Deserialize)] +struct MinimumDisjunctiveNormalFormData { + num_variables: usize, + truth_table: Vec, +} + +impl TryFrom for MinimumDisjunctiveNormalForm { + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumDisjunctiveNormalFormData) -> Result { + Self::new(data.num_variables, data.truth_table) + } +} + impl MinimumDisjunctiveNormalForm { /// Create a new MinimumDisjunctiveNormalForm problem. /// - /// # Panics + /// # Errors /// - If truth_table length != 2^num_variables /// - If the function is identically false (no minterms) - pub fn new(num_variables: usize, truth_table: Vec) -> Self { - assert!(num_variables >= 1, "Need at least 1 variable"); - assert_eq!( - truth_table.len(), - 1 << num_variables, - "Truth table must have 2^n entries" - ); + pub fn new( + num_variables: usize, + truth_table: Vec, + ) -> Result { + if num_variables == 0 { + return Err("Need at least 1 variable".into()); + } + if truth_table.len() + != 1usize + .checked_shl( + u32::try_from(num_variables).map_err(|_| "truth table size overflows usize")?, + ) + .ok_or("truth table size overflows usize")? + { + return Err("Truth table must have 2^n entries".into()); + } let minterms: Vec = truth_table .iter() .enumerate() .filter_map(|(i, &v)| if v { Some(i) } else { None }) .collect(); - assert!( - !minterms.is_empty(), - "Function must have at least one minterm" - ); + if minterms.is_empty() { + return Err("Function must have at least one minterm".into()); + } let prime_implicants = compute_prime_implicants(num_variables, &minterms); - Self { + Ok(Self { num_variables, truth_table, prime_implicants, minterms, - } + }) } /// Get the number of variables. @@ -197,8 +219,12 @@ impl Problem for MinimumDisjunctiveNormalForm { } impl crate::solvers::BruteForceProblem for MinimumDisjunctiveNormalForm { - fn dimensions(&self) -> Vec { - vec![2; self.prime_implicants.len()] + fn num_variables(&self) -> Result { + Ok(self.prime_implicants.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -301,10 +327,13 @@ fn try_merge(a: &[Option], b: &[Option]) -> Option> pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "minimum_disjunctive_normal_form", - instance: Box::new(MinimumDisjunctiveNormalForm::new( - 3, - vec![false, true, true, true, true, true, true, false], - )), + instance: Box::new( + MinimumDisjunctiveNormalForm::new( + 3, + vec![false, true, true, true, true, true, true, false], + ) + .unwrap(), + ), // Select prime implicants: p1(¬x1∧x2), p4(x1∧¬x3), p5(¬x2∧x3) // The order of PIs depends on the QMC algorithm output. // We'll verify this in tests. diff --git a/src/models/misc/minimum_external_macro_data_compression.rs b/src/models/misc/minimum_external_macro_data_compression.rs index dbaa37c8f..de4c4d500 100644 --- a/src/models/misc/minimum_external_macro_data_compression.rs +++ b/src/models/misc/minimum_external_macro_data_compression.rs @@ -63,7 +63,7 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Alphabet {a, b}, string "abab", pointer cost h=2 -/// let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2); +/// let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); @@ -87,55 +87,39 @@ impl TryFrom for MinimumExternalMacroD type Error = crate::registry::ConstructionError; fn try_from(value: MinimumExternalMacroDataCompressionSerde) -> Result { - if value.alphabet_size == 0 && !value.string.is_empty() { - return Err("alphabet_size must be > 0 when the string is non-empty" - .to_string() - .into()); - } - if value - .string - .iter() - .any(|&symbol| symbol >= value.alphabet_size) - { - return Err("all symbols must be less than alphabet_size" - .to_string() - .into()); - } - if value.pointer_cost <= 0 { - return Err("pointer_cost must be positive".to_string().into()); - } - Ok(Self { - alphabet_size: value.alphabet_size, - string: value.string, - pointer_cost: value.pointer_cost, - }) + Self::new(value.alphabet_size, value.string, value.pointer_cost) } } impl MinimumExternalMacroDataCompression { /// Create a new MinimumExternalMacroDataCompression instance. /// - /// # Panics + /// # Errors /// - /// Panics if `alphabet_size` is 0 and the string is non-empty, or if + /// Returns an error if `alphabet_size` is 0 and the string is non-empty, or if /// any symbol in the string is >= `alphabet_size`, or if `pointer_cost` is 0. - pub fn new(alphabet_size: usize, string: Vec, pointer_cost: i64) -> Self { - assert!( - alphabet_size > 0 || string.is_empty(), - "alphabet_size must be > 0 when the string is non-empty" - ); - assert!( - string - .iter() - .all(|&s| s < alphabet_size || alphabet_size == 0), - "all symbols must be less than alphabet_size" - ); - assert!(pointer_cost > 0, "pointer_cost must be positive"); - Self { + pub fn new( + alphabet_size: usize, + string: Vec, + pointer_cost: i64, + ) -> Result { + if !(alphabet_size > 0 || string.is_empty()) { + return Err("alphabet_size must be > 0 when the string is non-empty".into()); + }; + if !(string + .iter() + .all(|&s| s < alphabet_size || alphabet_size == 0)) + { + return Err("all symbols must be less than alphabet_size".into()); + }; + if pointer_cost <= 0 { + return Err("pointer_cost must be positive".into()); + }; + Ok(Self { alphabet_size, string, pointer_cost, - } + }) } /// Returns the length of the source string. @@ -158,17 +142,6 @@ impl MinimumExternalMacroDataCompression { &self.string } - /// Returns the number of valid pointers into D (|s|*(|s|+1)/2). - fn num_pointers(&self) -> usize { - let n = self.string.len(); - n * (n + 1) / 2 - } - - /// Returns the C-slot domain size: alphabet_size + 1 (empty) + num_pointers. - fn c_domain_size(&self) -> usize { - self.alphabet_size + 1 + self.num_pointers() - } - /// Decode a pointer index (offset from alphabet_size+1) into (start, len) /// in the dictionary. Pointers are enumerated as: /// index 0 -> (0, 1), 1 -> (0, 2), ..., n-1 -> (0, n), @@ -322,13 +295,22 @@ impl Problem for MinimumExternalMacroDataCompression { } impl crate::solvers::BruteForceProblem for MinimumExternalMacroDataCompression { - fn dimensions(&self) -> Vec { - let n = self.string.len(); - let d_domain = self.alphabet_size + 1; // symbols + empty - let c_domain = self.c_domain_size(); // symbols + empty + pointers - let mut dims = vec![d_domain; n]; // D-slots - dims.extend(vec![c_domain; n]); // C-slots - dims + fn num_variables(&self) -> Result { + Ok(2 * self.string.len()) + } + + fn dimension(&self, variable: usize) -> Result { + if variable < self.string.len() { + Ok((self.alphabet_size).checked_add(1usize).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a search coordinate size".into(), + ) + })?) + } else { + let n = self.string.len() as u128; + let cardinality = self.alphabet_size as u128 + 1 + n * (n + 1) / 2; + Ok(usize::try_from(cardinality)?) + } } } @@ -357,7 +339,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Deserialize<'de> for MinimumFaultDetectionTestSet { D: Deserializer<'de>, { let data = MinimumFaultDetectionTestSetData::deserialize(deserializer)?; - let coverage = - Self::build_coverage(data.num_vertices, &data.arcs, &data.inputs, &data.outputs); - Ok(Self { - num_vertices: data.num_vertices, - arcs: data.arcs, - inputs: data.inputs, - outputs: data.outputs, - coverage, - }) + Self::new(data.num_vertices, data.arcs, data.inputs, data.outputs) + .map_err(serde::de::Error::custom) } } impl MinimumFaultDetectionTestSet { /// Create a new Minimum Fault Detection Test Set instance. /// - /// # Panics + /// # Errors /// - /// Panics if any arc index is out of bounds, if any input or output index + /// Returns an error if any arc index is out of bounds, if any input or output index /// is out of bounds, or if inputs or outputs are empty. pub fn new( num_vertices: usize, arcs: Vec<(usize, usize)>, inputs: Vec, outputs: Vec, - ) -> Self { - assert!(!inputs.is_empty(), "Inputs must not be empty"); - assert!(!outputs.is_empty(), "Outputs must not be empty"); + ) -> Result { + if inputs.is_empty() { + return Err("Inputs must not be empty".into()); + }; + if outputs.is_empty() { + return Err("Outputs must not be empty".into()); + }; for (i, &(u, v)) in arcs.iter().enumerate() { - assert!( - u < num_vertices && v < num_vertices, - "Arc {} ({}, {}) out of bounds for {} vertices", - i, - u, - v, - num_vertices - ); + if !(u < num_vertices && v < num_vertices) { + return Err(format!( + "Arc {} ({}, {}) out of bounds for {} vertices", + i, u, v, num_vertices + ) + .into()); + }; } for &inp in &inputs { - assert!( - inp < num_vertices, - "Input vertex {} out of bounds for {} vertices", - inp, - num_vertices - ); + if !(inp < num_vertices) { + return Err(format!( + "Input vertex {} out of bounds for {} vertices", + inp, num_vertices + ) + .into()); + }; } for &out in &outputs { - assert!( - out < num_vertices, - "Output vertex {} out of bounds for {} vertices", - out, - num_vertices - ); + if !(out < num_vertices) { + return Err(format!( + "Output vertex {} out of bounds for {} vertices", + out, num_vertices + ) + .into()); + }; } let coverage = Self::build_coverage(num_vertices, &arcs, &inputs, &outputs); - Self { + Ok(Self { num_vertices, arcs, inputs, outputs, coverage, - } + }) } /// Compute forward reachability from a given vertex using BFS on the DAG. @@ -335,8 +333,16 @@ impl Problem for MinimumFaultDetectionTestSet { } impl crate::solvers::BruteForceProblem for MinimumFaultDetectionTestSet { - fn dimensions(&self) -> Vec { - vec![2; self.inputs.len() * self.outputs.len()] + fn num_variables(&self) -> Result { + (self.inputs.len()) + .checked_mul(self.outputs.len()) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -357,21 +363,24 @@ pub(crate) fn canonical_model_example_specs() -> Vec covers all internal vertices -> Min(2) vec![crate::example_db::specs::ModelExampleSpec { id: "minimum_fault_detection_test_set", - instance: Box::new(MinimumFaultDetectionTestSet::new( - 7, - vec![ - (0, 2), - (0, 3), - (1, 3), - (1, 4), - (2, 5), - (3, 5), - (3, 6), - (4, 6), - ], - vec![0, 1], - vec![5, 6], - )), + instance: Box::new( + MinimumFaultDetectionTestSet::new( + 7, + vec![ + (0, 2), + (0, 3), + (1, 3), + (1, 4), + (2, 5), + (3, 5), + (3, 6), + (4, 6), + ], + vec![0, 1], + vec![5, 6], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![vec![true, false], vec![false, true]]), optimal_value: serde_json::json!(2), }] diff --git a/src/models/misc/minimum_internal_macro_data_compression.rs b/src/models/misc/minimum_internal_macro_data_compression.rs index 611981db0..923e8fa85 100644 --- a/src/models/misc/minimum_internal_macro_data_compression.rs +++ b/src/models/misc/minimum_internal_macro_data_compression.rs @@ -60,7 +60,7 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Alphabet {a, b}, string "abab", pointer cost h=2 -/// let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2); +/// let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); @@ -84,55 +84,39 @@ impl TryFrom for MinimumInternalMacroD type Error = crate::registry::ConstructionError; fn try_from(value: MinimumInternalMacroDataCompressionSerde) -> Result { - if value.alphabet_size == 0 && !value.string.is_empty() { - return Err("alphabet_size must be > 0 when the string is non-empty" - .to_string() - .into()); - } - if value - .string - .iter() - .any(|&symbol| symbol >= value.alphabet_size) - { - return Err("all symbols must be less than alphabet_size" - .to_string() - .into()); - } - if value.pointer_cost <= 0 { - return Err("pointer_cost must be positive".to_string().into()); - } - Ok(Self { - alphabet_size: value.alphabet_size, - string: value.string, - pointer_cost: value.pointer_cost, - }) + Self::new(value.alphabet_size, value.string, value.pointer_cost) } } impl MinimumInternalMacroDataCompression { /// Create a new MinimumInternalMacroDataCompression instance. /// - /// # Panics + /// # Errors /// - /// Panics if `alphabet_size` is 0 and the string is non-empty, or if + /// Returns an error if `alphabet_size` is 0 and the string is non-empty, or if /// any symbol in the string is >= `alphabet_size`, or if `pointer_cost` is 0. - pub fn new(alphabet_size: usize, string: Vec, pointer_cost: i64) -> Self { - assert!( - alphabet_size > 0 || string.is_empty(), - "alphabet_size must be > 0 when the string is non-empty" - ); - assert!( - string - .iter() - .all(|&s| s < alphabet_size || alphabet_size == 0), - "all symbols must be less than alphabet_size" - ); - assert!(pointer_cost > 0, "pointer_cost must be positive"); - Self { + pub fn new( + alphabet_size: usize, + string: Vec, + pointer_cost: i64, + ) -> Result { + if !(alphabet_size > 0 || string.is_empty()) { + return Err("alphabet_size must be > 0 when the string is non-empty".into()); + }; + if !(string + .iter() + .all(|&s| s < alphabet_size || alphabet_size == 0)) + { + return Err("all symbols must be less than alphabet_size".into()); + }; + if pointer_cost <= 0 { + return Err("pointer_cost must be positive".into()); + }; + Ok(Self { alphabet_size, string, pointer_cost, - } + }) } /// Returns the length of the source string. @@ -285,10 +269,22 @@ impl Problem for MinimumInternalMacroDataCompression { } impl crate::solvers::BruteForceProblem for MinimumInternalMacroDataCompression { - fn dimensions(&self) -> Vec { - let n = self.string.len(); - let domain = self.alphabet_size + n + 1; // literals + EOS + pointers - vec![domain; n] + fn num_variables(&self) -> Result { + Ok(self.string.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + ((self.alphabet_size) + .checked_add(self.string.len()) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a coordinate cardinality".into(), + ) + })?) + .checked_add(1usize) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } @@ -321,7 +317,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec, } +#[derive(Deserialize)] +struct MinimumRegisterSufficiencyForLoopsData { + loop_length: usize, + variables: Vec<(usize, usize)>, +} + +impl TryFrom for MinimumRegisterSufficiencyForLoops { + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumRegisterSufficiencyForLoopsData) -> Result { + Self::new(data.loop_length, data.variables) + } +} + impl MinimumRegisterSufficiencyForLoops { /// Create a new Minimum Register Sufficiency for Loops instance. /// - /// # Panics + /// # Errors /// - /// Panics if `loop_length` is zero, if any duration is zero or exceeds + /// Returns an error if `loop_length` is zero, if any duration is zero or exceeds /// `loop_length`, or if any `start_time >= loop_length`. - pub fn new(loop_length: usize, variables: Vec<(usize, usize)>) -> Self { - assert!(loop_length > 0, "loop_length must be positive"); + pub fn new( + loop_length: usize, + variables: Vec<(usize, usize)>, + ) -> Result { + if loop_length == 0 { + return Err("loop_length must be positive".into()); + } for (i, &(start, dur)) in variables.iter().enumerate() { - assert!( - start < loop_length, - "Variable {} start_time {} >= loop_length {}", - i, - start, - loop_length - ); - assert!( - dur > 0 && dur <= loop_length, - "Variable {} duration {} must be in [1, {}]", - i, - dur, - loop_length - ); + if !(start < loop_length) { + return Err(format!( + "Variable {} start_time {} >= loop_length {}", + i, start, loop_length + ) + .into()); + } + if !(dur > 0 && dur <= loop_length) { + return Err(format!( + "Variable {} duration {} must be in [1, {}]", + i, dur, loop_length + ) + .into()); + } } - Self { + Ok(Self { loop_length, variables, - } + }) } /// Get the loop length N. @@ -212,9 +231,12 @@ impl Problem for MinimumRegisterSufficiencyForLoops { } impl crate::solvers::BruteForceProblem for MinimumRegisterSufficiencyForLoops { - fn dimensions(&self) -> Vec { - let n = self.variables.len(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.variables.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.variables.len()) } } @@ -232,10 +254,9 @@ pub(crate) fn canonical_model_example_specs() -> Vec { lengths: Vec, deadlines: Vec, precedences: Vec<(usize, usize)>, } -macro_rules! minimum_tardiness_create_spec { - ($name:ident, $weight:ty, $construct:expr) => { - #[derive(Debug, Deserialize, crate::CreateSpec)] - struct $name { - lengths: Vec<$weight>, - deadlines: Vec, - precedences: Option>, - } +#[derive(Deserialize)] +struct MinimumTardinessSequencingData { + lengths: Vec, + deadlines: Vec, + precedences: Vec<(usize, usize)>, +} - impl TryFrom<$name> for MinimumTardinessSequencing<$weight> { - type Error = crate::registry::ConstructionError; +impl<'de> Deserialize<'de> for MinimumTardinessSequencing { + fn deserialize>(deserializer: D) -> Result { + let data = MinimumTardinessSequencingData::::deserialize(deserializer)?; + Self::new(data.lengths.len(), data.deadlines, data.precedences) + .map_err(serde::de::Error::custom) + } +} - fn try_from(spec: $name) -> Result { - if spec.lengths.len() != spec.deadlines.len() { - return Err("lengths and deadlines must have the same length" - .to_string() - .into()); - } - let precedences = spec.precedences.unwrap_or_default(); - let num_tasks = spec.lengths.len(); - if let Some(&(pred, succ)) = precedences - .iter() - .find(|&&(pred, succ)| pred >= num_tasks || succ >= num_tasks) - { - return Err(format!( - "precedence ({pred}, {succ}) is out of range for {num_tasks} tasks" - ) - .into()); - } - $construct(spec.lengths, spec.deadlines, precedences) - } - } - }; +impl<'de> Deserialize<'de> for MinimumTardinessSequencing { + fn deserialize>(deserializer: D) -> Result { + let data = MinimumTardinessSequencingData::::deserialize(deserializer)?; + Self::with_lengths(data.lengths, data.deadlines, data.precedences) + .map_err(serde::de::Error::custom) + } } #[derive(Debug, Deserialize, crate::CreateSpec)] @@ -104,101 +92,93 @@ struct MinimumTardinessSequencingOneCreateSpec { impl TryFrom for MinimumTardinessSequencing { type Error = crate::registry::ConstructionError; fn try_from(spec: MinimumTardinessSequencingOneCreateSpec) -> Result { - let num_tasks = spec.deadlines.len(); - let precedences = spec.precedences.unwrap_or_default(); - if precedences - .iter() - .any(|&(a, b)| a >= num_tasks || b >= num_tasks) - { - return Err("precedence indices must be within the task count".into()); - } - Ok(Self::new(num_tasks, spec.deadlines, precedences)) + Self::new( + spec.deadlines.len(), + spec.deadlines, + spec.precedences.unwrap_or_default(), + ) } } -minimum_tardiness_create_spec!( - MinimumTardinessSequencingI64CreateSpec, - i64, - |lengths: Vec, deadlines, precedences| { - if lengths.iter().any(|&length| length <= 0) { - return Err("all task lengths must be positive".to_string().into()); - } - Ok(MinimumTardinessSequencing::with_lengths( - lengths, - deadlines, - precedences, - )) +#[derive(Debug, Deserialize, crate::CreateSpec)] +struct MinimumTardinessSequencingI64CreateSpec { + lengths: Vec, + deadlines: Vec, + precedences: Option>, +} +impl TryFrom for MinimumTardinessSequencing { + type Error = crate::registry::ConstructionError; + fn try_from(spec: MinimumTardinessSequencingI64CreateSpec) -> Result { + Self::with_lengths( + spec.lengths, + spec.deadlines, + spec.precedences.unwrap_or_default(), + ) } -); +} impl MinimumTardinessSequencing { /// Create a new unit-length MinimumTardinessSequencing instance. /// - /// # Panics + /// # Errors /// - /// Panics if `deadlines.len() != num_tasks` or if any task index in `precedences` + /// Returns an error if `deadlines.len() != num_tasks` or if any task index in `precedences` /// is out of range. - pub fn new(num_tasks: usize, deadlines: Vec, precedences: Vec<(usize, usize)>) -> Self { - assert_eq!( - deadlines.len(), - num_tasks, - "deadlines length must equal num_tasks" - ); - validate_precedences(num_tasks, &precedences); - Self { + pub fn new( + num_tasks: usize, + deadlines: Vec, + precedences: Vec<(usize, usize)>, + ) -> Result { + validate_task_data(num_tasks, &deadlines, &precedences)?; + Ok(Self { lengths: vec![One; num_tasks], deadlines, precedences, - } + }) } } impl MinimumTardinessSequencing { /// Create a new arbitrary-length MinimumTardinessSequencing instance. /// - /// # Panics + /// # Errors /// - /// Panics if `lengths.len() != deadlines.len()`, if any length is 0, + /// Returns an error if `lengths.len() != deadlines.len()`, if any length is 0, /// or if any task index in `precedences` is out of range. pub fn with_lengths( lengths: Vec, deadlines: Vec, precedences: Vec<(usize, usize)>, - ) -> Self { - assert_eq!( - lengths.len(), - deadlines.len(), - "lengths and deadlines must have the same length" - ); - assert!( - lengths.iter().all(|&l| l > 0), - "all task lengths must be positive" - ); - let num_tasks = lengths.len(); - validate_precedences(num_tasks, &precedences); - Self { + ) -> Result { + validate_task_data(lengths.len(), &deadlines, &precedences)?; + if lengths.iter().any(|&length| length <= 0) { + return Err("all task lengths must be positive".into()); + } + Ok(Self { lengths, deadlines, precedences, - } + }) } } -fn validate_precedences(num_tasks: usize, precedences: &[(usize, usize)]) { +fn validate_task_data( + num_tasks: usize, + deadlines: &[i64], + precedences: &[(usize, usize)], +) -> Result<(), crate::registry::ConstructionError> { + if deadlines.len() != num_tasks { + return Err("deadlines length must equal task count".into()); + } for &(pred, succ) in precedences { - assert!( - pred < num_tasks, - "predecessor index {} out of range (num_tasks = {})", - pred, - num_tasks - ); - assert!( - succ < num_tasks, - "successor index {} out of range (num_tasks = {})", - succ, - num_tasks - ); + if pred >= num_tasks || succ >= num_tasks { + return Err(format!( + "precedence ({pred}, {succ}) is out of range for {num_tasks} tasks" + ) + .into()); + } } + Ok(()) } impl MinimumTardinessSequencing { @@ -308,8 +288,12 @@ impl Problem for MinimumTardinessSequencing { } impl crate::solvers::BruteForceProblem for MinimumTardinessSequencing { - fn dimensions(&self) -> Vec { - super::lehmer_dims(self.num_tasks()) + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.num_tasks() - variable) } } @@ -382,8 +366,12 @@ impl Problem for MinimumTardinessSequencing { } impl crate::solvers::BruteForceProblem for MinimumTardinessSequencing { - fn dimensions(&self) -> Vec { - super::lehmer_dims(self.num_tasks()) + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.num_tasks() - variable) } } @@ -403,11 +391,9 @@ pub(crate) fn canonical_model_example_specs() -> Vec::new( - 4, - vec![2, 3, 1, 4], - vec![(0, 2)], - )), + instance: Box::new( + MinimumTardinessSequencing::::new(4, vec![2, 3, 1, 4], vec![(0, 2)]).unwrap(), + ), optimal_config: serde_json::json!(vec![0, 1, 2, 3]), optimal_value: serde_json::json!(1), }, @@ -418,11 +404,14 @@ pub(crate) fn canonical_model_example_specs() -> Vec::with_lengths( - vec![3, 2, 2, 1, 2], - vec![4, 3, 8, 3, 6], - vec![(0, 2), (1, 3)], - )), + instance: Box::new( + MinimumTardinessSequencing::::with_lengths( + vec![3, 2, 2, 1, 2], + vec![4, 3, 8, 3, 6], + vec![(0, 2), (1, 3)], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![0, 4, 2, 1, 3]), optimal_value: serde_json::json!(2), }, diff --git a/src/models/misc/minimum_weight_and_or_graph.rs b/src/models/misc/minimum_weight_and_or_graph.rs index 879879967..a2da7f7ae 100644 --- a/src/models/misc/minimum_weight_and_or_graph.rs +++ b/src/models/misc/minimum_weight_and_or_graph.rs @@ -50,7 +50,7 @@ inventory::submit! { /// 0, /// vec![Some(true), Some(false), Some(false), None, None, None, None], /// vec![1, 2, 3, 1, 4, 2], -/// ); +/// ).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap().unwrap(); /// assert_eq!(problem.evaluate(&solution).unwrap(), problemreductions::types::Min(Some(6))); @@ -88,40 +88,14 @@ struct MinimumWeightAndOrGraphCreateSpec { impl TryFrom for MinimumWeightAndOrGraph { type Error = crate::registry::ConstructionError; fn try_from(spec: MinimumWeightAndOrGraphCreateSpec) -> Result { - if spec.source >= spec.num_vertices { - return Err("source is outside the graph".to_string().into()); - } - if spec.gate_types.len() != spec.num_vertices { - return Err("gate_types length must equal num_vertices" - .to_string() - .into()); - } - if spec.gate_types[spec.source].is_none() { - return Err("source must be an AND or OR gate".to_string().into()); - } - if let Some(&(u, v)) = spec - .arcs - .iter() - .find(|&&(u, v)| u >= spec.num_vertices || v >= spec.num_vertices) - { - return Err(format!("arc ({u}, {v}) is out of bounds").into()); - } - let count = spec.arcs.len(); - let arc_weights = spec.arc_weights.unwrap_or_else(|| vec![1; count]); - if arc_weights.len() != count { - return Err(format!( - "arc_weights has {} entries, expected {count}", - arc_weights.len() - ) - .into()); - } - Ok(Self::new( + let arc_weights = spec.arc_weights.unwrap_or_else(|| vec![1; spec.arcs.len()]); + Self::new( spec.num_vertices, spec.arcs, spec.source, spec.gate_types, arc_weights, - )) + ) } } @@ -140,24 +114,23 @@ impl<'de> Deserialize<'de> for MinimumWeightAndOrGraph { D: Deserializer<'de>, { let data = MinimumWeightAndOrGraphData::deserialize(deserializer)?; - let outgoing = Self::build_outgoing(data.num_vertices, &data.arcs); - Ok(Self { - num_vertices: data.num_vertices, - arcs: data.arcs, - source: data.source, - gate_types: data.gate_types, - arc_weights: data.arc_weights, - outgoing, - }) + Self::new( + data.num_vertices, + data.arcs, + data.source, + data.gate_types, + data.arc_weights, + ) + .map_err(serde::de::Error::custom) } } impl MinimumWeightAndOrGraph { /// Create a new Minimum Weight AND/OR Graph instance. /// - /// # Panics + /// # Errors /// - /// Panics if any arc index is out of bounds, if the source is out of bounds, + /// Returns an error if any arc index is out of bounds, if the source is out of bounds, /// if gate_types length does not match num_vertices, if arc_weights length /// does not match the number of arcs, or if the source is a leaf. pub fn new( @@ -166,50 +139,51 @@ impl MinimumWeightAndOrGraph { source: usize, gate_types: Vec>, arc_weights: Vec, - ) -> Self { - assert!( - source < num_vertices, - "Source vertex {} out of bounds for {} vertices", - source, - num_vertices - ); - assert_eq!( - gate_types.len(), - num_vertices, - "gate_types length {} does not match num_vertices {}", - gate_types.len(), - num_vertices - ); - assert_eq!( - arc_weights.len(), - arcs.len(), - "arc_weights length {} does not match number of arcs {}", - arc_weights.len(), - arcs.len() - ); - for (i, &(u, v)) in arcs.iter().enumerate() { - assert!( - u < num_vertices && v < num_vertices, - "Arc {} ({}, {}) out of bounds for {} vertices", - i, - u, - v, + ) -> Result { + if !(source < num_vertices) { + return Err(format!( + "Source vertex {} out of bounds for {} vertices", + source, num_vertices + ) + .into()); + }; + if gate_types.len() != num_vertices { + return Err(format!( + "gate_types length {} does not match num_vertices {}", + gate_types.len(), num_vertices - ); + ) + .into()); + }; + if arc_weights.len() != arcs.len() { + return Err(format!( + "arc_weights length {} does not match number of arcs {}", + arc_weights.len(), + arcs.len() + ) + .into()); + }; + for (i, &(u, v)) in arcs.iter().enumerate() { + if !(u < num_vertices && v < num_vertices) { + return Err(format!( + "Arc {} ({}, {}) out of bounds for {} vertices", + i, u, v, num_vertices + ) + .into()); + }; } - assert!( - gate_types[source].is_some(), - "Source vertex must be an AND or OR gate, not a leaf" - ); + if !(gate_types[source].is_some()) { + return Err("Source vertex must be an AND or OR gate, not a leaf".into()); + }; let outgoing = Self::build_outgoing(num_vertices, &arcs); - Self { + Ok(Self { num_vertices, arcs, source, gate_types, arc_weights, outgoing, - } + }) } /// Build outgoing arc index lists for each vertex. @@ -350,8 +324,12 @@ impl Problem for MinimumWeightAndOrGraph { } impl crate::solvers::BruteForceProblem for MinimumWeightAndOrGraph { - fn dimensions(&self) -> Vec { - vec![2; self.arcs.len()] + fn num_variables(&self) -> Result { + Ok(self.arcs.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -388,13 +366,16 @@ pub(crate) fn canonical_model_example_specs() -> Vec, /// Number of identical processors. - #[serde(deserialize_with = "positive_usize::deserialize")] num_processors: usize, /// Global deadline. deadline: i64, @@ -72,30 +72,34 @@ struct MultiprocessorSchedulingCreateSpec { impl TryFrom for MultiprocessorScheduling { type Error = crate::registry::ConstructionError; fn try_from(spec: MultiprocessorSchedulingCreateSpec) -> Result { - if spec.num_processors == 0 { - return Err("num_processors must be positive".to_string().into()); - } - Ok(Self::new(spec.lengths, spec.num_processors, spec.deadline)) + Self::new(spec.lengths, spec.num_processors, spec.deadline) } } impl MultiprocessorScheduling { /// Create a new Multiprocessor Scheduling instance. /// - /// # Panics - /// Panics if `num_processors` is zero. - pub fn new(lengths: Vec, num_processors: usize, deadline: i64) -> Self { - assert!(num_processors > 0, "num_processors must be positive"); - assert!( - lengths.iter().all(|&length| length >= 0), - "task lengths must be nonnegative" - ); - assert!(deadline >= 0, "deadline must be nonnegative"); - Self { + /// # Errors + /// Returns an error if `num_processors` is zero. + pub fn new( + lengths: Vec, + num_processors: usize, + deadline: i64, + ) -> Result { + if num_processors == 0 { + return Err("num_processors must be positive".into()); + } + if !(lengths.iter().all(|&length| length >= 0)) { + return Err("task lengths must be nonnegative".into()); + } + if !(deadline >= 0) { + return Err("deadline must be nonnegative".into()); + } + Ok(Self { lengths, num_processors, deadline, - } + }) } /// Returns the processing times for each task. @@ -170,8 +174,12 @@ impl Problem for MultiprocessorScheduling { } impl crate::solvers::BruteForceProblem for MultiprocessorScheduling { - fn dimensions(&self) -> Vec { - vec![self.num_processors; self.num_tasks()] + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_processors) } } @@ -187,28 +195,12 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "multiprocessor_scheduling", - instance: Box::new(MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10)), + instance: Box::new(MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10).unwrap()), optimal_config: serde_json::json!(vec![0, 1, 1, 1, 0]), optimal_value: serde_json::json!(true), }] } -mod positive_usize { - use serde::de::Error; - use serde::{Deserialize, Deserializer}; - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = usize::deserialize(deserializer)?; - if value == 0 { - return Err(D::Error::custom("expected positive integer, got 0")); - } - Ok(value) - } -} - #[cfg(test)] #[path = "../../unit_tests/models/misc/multiprocessor_scheduling.rs"] mod tests; diff --git a/src/models/misc/non_liveness_free_petri_net.rs b/src/models/misc/non_liveness_free_petri_net.rs index 66fca114a..70c643ee4 100644 --- a/src/models/misc/non_liveness_free_petri_net.rs +++ b/src/models/misc/non_liveness_free_petri_net.rs @@ -429,8 +429,12 @@ impl Problem for NonLivenessFreePetriNet { } impl crate::solvers::BruteForceProblem for NonLivenessFreePetriNet { - fn dimensions(&self) -> Vec { - vec![2; self.num_transitions] + fn num_variables(&self) -> Result { + Ok(self.num_transitions) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/numerical_3_dimensional_matching.rs b/src/models/misc/numerical_3_dimensional_matching.rs index 2a07faf11..9e3d72ff1 100644 --- a/src/models/misc/numerical_3_dimensional_matching.rs +++ b/src/models/misc/numerical_3_dimensional_matching.rs @@ -217,8 +217,12 @@ impl Problem for Numerical3DimensionalMatching { } impl crate::solvers::BruteForceProblem for Numerical3DimensionalMatching { - fn dimensions(&self) -> Vec { - vec![self.num_groups(); 2 * self.num_groups()] + fn num_variables(&self) -> Result { + Ok(2 * self.num_groups()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_groups()) } } diff --git a/src/models/misc/numerical_matching_with_target_sums.rs b/src/models/misc/numerical_matching_with_target_sums.rs index bccdc2f7c..c7e653acf 100644 --- a/src/models/misc/numerical_matching_with_target_sums.rs +++ b/src/models/misc/numerical_matching_with_target_sums.rs @@ -173,9 +173,12 @@ impl Problem for NumericalMatchingWithTargetSums { } impl crate::solvers::BruteForceProblem for NumericalMatchingWithTargetSums { - fn dimensions(&self) -> Vec { - let m = self.num_pairs(); - vec![m; m] + fn num_variables(&self) -> Result { + Ok(self.num_pairs()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_pairs()) } } diff --git a/src/models/misc/open_shop_scheduling.rs b/src/models/misc/open_shop_scheduling.rs index 30680b8cc..9aad80df4 100644 --- a/src/models/misc/open_shop_scheduling.rs +++ b/src/models/misc/open_shop_scheduling.rs @@ -138,7 +138,7 @@ impl OpenShopScheduling { "operation count overflows usize".into(), ) })?; - let horizon = processing_times + processing_times .iter() .flatten() .try_fold(0i64, |total, &time| total.checked_add(time)) @@ -147,14 +147,6 @@ impl OpenShopScheduling { "schedule horizon overflows i64".into(), ) })?; - usize::try_from(horizon) - .ok() - .and_then(|value| value.checked_add(1)) - .ok_or_else(|| { - crate::registry::ConstructionError::IntegerOverflow( - "schedule horizon domain overflows usize".into(), - ) - })?; Ok(Self { num_machines, processing_times, @@ -177,16 +169,8 @@ impl OpenShopScheduling { } /// Return the sum of all processing times, a valid serial-schedule horizon. - pub fn schedule_horizon(&self) -> usize { - self.processing_times - .iter() - .flatten() - .try_fold(0usize, |total, &time| { - usize::try_from(time) - .ok() - .and_then(|time| total.checked_add(time)) - }) - .expect("processing times must fit the brute-force schedule horizon") + pub fn schedule_horizon(&self) -> i64 { + self.processing_times.iter().flatten().sum() } fn finish_time( @@ -284,12 +268,12 @@ impl Problem for OpenShopScheduling { } impl crate::solvers::BruteForceProblem for OpenShopScheduling { - fn dimensions(&self) -> Vec { - let domain = self - .schedule_horizon() - .checked_add(1) - .expect("schedule horizon overflow"); - vec![domain; self.num_jobs() * self.num_machines] + fn num_variables(&self) -> Result { + Ok(self.num_jobs() * self.num_machines) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.schedule_horizon()) + 1)?) } } diff --git a/src/models/misc/optimum_communication_spanning_tree.rs b/src/models/misc/optimum_communication_spanning_tree.rs index a2732e7a1..67416a7e6 100644 --- a/src/models/misc/optimum_communication_spanning_tree.rs +++ b/src/models/misc/optimum_communication_spanning_tree.rs @@ -57,18 +57,32 @@ inventory::submit! { /// vec![1, 0, 1], /// vec![1, 1, 0], /// ], -/// ); +/// ).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "OptimumCommunicationSpanningTreeData")] pub struct OptimumCommunicationSpanningTree { num_vertices: usize, edge_weights: Vec>, requirements: Vec>, } +#[derive(Deserialize)] +struct OptimumCommunicationSpanningTreeData { + edge_weights: Vec>, + requirements: Vec>, +} + +impl TryFrom for OptimumCommunicationSpanningTree { + type Error = crate::registry::ConstructionError; + fn try_from(data: OptimumCommunicationSpanningTreeData) -> Result { + Self::new(data.edge_weights, data.requirements) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct OptimumCommunicationSpanningTreeCreateSpec { /// Number of vertices. @@ -82,33 +96,15 @@ impl TryFrom for OptimumCommunicatio type Error = crate::registry::ConstructionError; fn try_from(spec: OptimumCommunicationSpanningTreeCreateSpec) -> Result { let n = spec.num_vertices; - if n < 2 { - return Err("must have at least two vertices".to_string().into()); - } let edge_weights = spec.edge_weights.unwrap_or_else(|| { (0..n) .map(|i| (0..n).map(|j| i64::from(i != j)).collect()) .collect() }); - for (name, matrix) in [ - ("edge_weights", &edge_weights), - ("requirements", &spec.requirements), - ] { - if matrix.len() != n || matrix.iter().any(|row| row.len() != n) { - return Err(format!("{name} must be a {n} x {n} matrix").into()); - } - for (i, row) in matrix.iter().enumerate() { - if row[i] != 0 { - return Err(format!("{name} diagonal must be zero").into()); - } - for (j, &value) in row.iter().enumerate().skip(i + 1) { - if value != matrix[j][i] || value < 0 { - return Err(format!("{name} must be symmetric and nonnegative").into()); - } - } - } + if edge_weights.len() != n { + return Err("edge_weights row count must equal num_vertices".into()); } - Ok(Self::new(edge_weights, spec.requirements)) + Self::new(edge_weights, spec.requirements) } } @@ -120,78 +116,95 @@ impl OptimumCommunicationSpanningTree { /// * `edge_weights` - Symmetric n x n matrix with w(i,i) = 0 and w(i,j) >= 0. /// * `requirements` - Symmetric n x n matrix with r(i,i) = 0 and r(i,j) >= 0. /// - /// # Panics + /// # Errors /// - /// Panics if the matrices are not square, not the same size, have nonzero + /// Returns an error if the matrices are not square, not the same size, have nonzero /// diagonals, are not symmetric, or contain negative entries. - pub fn new(edge_weights: Vec>, requirements: Vec>) -> Self { + pub fn new( + edge_weights: Vec>, + requirements: Vec>, + ) -> Result { let n = edge_weights.len(); - assert!(n >= 2, "must have at least 2 vertices"); - assert_eq!( - requirements.len(), - n, - "requirements matrix must have same size as edge_weights" - ); + if !(n >= 2) { + return Err("must have at least 2 vertices".into()); + } + if requirements.len() != n { + return Err("requirements matrix must have same size as edge_weights".into()); + } for (i, row) in edge_weights.iter().enumerate() { - assert_eq!( - row.len(), - n, - "edge_weights must be square: row {i} has length {} but expected {n}", - row.len() - ); - assert_eq!( - row[i], 0, - "diagonal of edge_weights must be zero: edge_weights[{i}][{i}] = {}", - row[i] - ); + if row.len() != n { + return Err(format!( + "edge_weights must be square: row {i} has length {} but expected {n}", + row.len() + ) + .into()); + } + if row[i] != 0 { + return Err(format!( + "diagonal of edge_weights must be zero: edge_weights[{i}][{i}] = {}", + row[i] + ) + .into()); + } } for (i, row) in requirements.iter().enumerate() { - assert_eq!( - row.len(), - n, - "requirements must be square: row {i} has length {} but expected {n}", - row.len() - ); - assert_eq!( - row[i], 0, - "diagonal of requirements must be zero: requirements[{i}][{i}] = {}", - row[i] - ); + if row.len() != n { + return Err(format!( + "requirements must be square: row {i} has length {} but expected {n}", + row.len() + ) + .into()); + } + if row[i] != 0 { + return Err(format!( + "diagonal of requirements must be zero: requirements[{i}][{i}] = {}", + row[i] + ) + .into()); + } } // Check symmetry and non-negativity for i in 0..n { for j in (i + 1)..n { - assert_eq!( - edge_weights[i][j], edge_weights[j][i], - "edge_weights must be symmetric: w[{i}][{j}]={} != w[{j}][{i}]={}", - edge_weights[i][j], edge_weights[j][i] - ); - assert!( - edge_weights[i][j] >= 0, - "edge_weights must be non-negative: w[{i}][{j}]={}", - edge_weights[i][j] - ); - assert_eq!( - requirements[i][j], requirements[j][i], - "requirements must be symmetric: r[{i}][{j}]={} != r[{j}][{i}]={}", - requirements[i][j], requirements[j][i] - ); - assert!( - requirements[i][j] >= 0, - "requirements must be non-negative: r[{i}][{j}]={}", - requirements[i][j] - ); + if edge_weights[i][j] != edge_weights[j][i] { + return Err(format!( + "edge_weights must be symmetric: w[{i}][{j}]={} != w[{j}][{i}]={}", + edge_weights[i][j], edge_weights[j][i] + ) + .into()); + } + if !(edge_weights[i][j] >= 0) { + return Err(format!( + "edge_weights must be non-negative: w[{i}][{j}]={}", + edge_weights[i][j] + ) + .into()); + } + if requirements[i][j] != requirements[j][i] { + return Err(format!( + "requirements must be symmetric: r[{i}][{j}]={} != r[{j}][{i}]={}", + requirements[i][j], requirements[j][i] + ) + .into()); + } + if !(requirements[i][j] >= 0) { + return Err(format!( + "requirements must be non-negative: r[{i}][{j}]={}", + requirements[i][j] + ) + .into()); + } } } - Self { + Ok(Self { num_vertices: n, edge_weights, requirements, - } + }) } /// Returns the number of vertices. @@ -369,8 +382,12 @@ impl Problem for OptimumCommunicationSpanningTree { } impl crate::solvers::BruteForceProblem for OptimumCommunicationSpanningTree { - fn dimensions(&self) -> Vec { - vec![2; self.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -407,10 +424,9 @@ pub(crate) fn canonical_model_example_specs() -> Vec config = [1, 0, 1, 0, 0, 1] vec![crate::example_db::specs::ModelExampleSpec { id: "optimum_communication_spanning_tree", - instance: Box::new(OptimumCommunicationSpanningTree::new( - edge_weights, - requirements, - )), + instance: Box::new( + OptimumCommunicationSpanningTree::new(edge_weights, requirements).unwrap(), + ), optimal_config: serde_json::json!(vec![true, false, true, false, false, true]), optimal_value: serde_json::json!(20), }] diff --git a/src/models/misc/paintshop.rs b/src/models/misc/paintshop.rs index e758c9faf..fcac09ac4 100644 --- a/src/models/misc/paintshop.rs +++ b/src/models/misc/paintshop.rs @@ -38,7 +38,7 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Sequence: a, b, a, c, c, b -/// let problem = PaintShop::new(vec!["a", "b", "a", "c", "c", "b"]); +/// let problem = PaintShop::new(vec!["a", "b", "a", "c", "c", "b"]).unwrap(); /// /// let solver = BruteForce::new(); /// let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -50,6 +50,7 @@ inventory::submit! { /// } /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "PaintShopData")] pub struct PaintShop { /// The sequence of car labels (as indices into unique cars). sequence_indices: Vec, @@ -61,42 +62,68 @@ pub struct PaintShop { num_cars: usize, } +#[derive(Deserialize)] +struct PaintShopData { + sequence_indices: Vec, + car_labels: Vec, +} + +impl TryFrom for PaintShop { + type Error = crate::registry::ConstructionError; + + fn try_from(data: PaintShopData) -> Result { + let sequence = data + .sequence_indices + .into_iter() + .map(|index| { + data.car_labels.get(index).ok_or_else(|| { + crate::registry::ConstructionError::from(format!( + "car index {index} is outside car_labels" + )) + }) + }) + .collect::, _>>()?; + Self::new(sequence) + } +} + impl PaintShop { /// Create a new Paint Shop problem from string labels. /// /// Each element in the sequence must appear exactly twice. - pub fn new>(sequence: Vec) -> Self { - let sequence: Vec = sequence.iter().map(|s| s.as_ref().to_string()).collect(); - Self::from_strings(sequence) - } - - /// Create from a vector of strings. - pub fn from_strings(sequence: Vec) -> Self { + pub fn new>( + sequence: Vec, + ) -> Result { // Build car-to-index mapping and count occurrences - let mut car_count: HashMap = HashMap::new(); - let mut car_to_index: HashMap = HashMap::new(); + let mut car_count: HashMap<&str, usize> = HashMap::new(); + let mut car_to_index: HashMap<&str, usize> = HashMap::new(); let mut car_labels: Vec = Vec::new(); for item in &sequence { - let count = car_count.entry(item.clone()).or_insert(0); + let item = item.as_ref(); + let count = car_count.entry(item).or_insert(0); if *count == 0 { - car_to_index.insert(item.clone(), car_labels.len()); - car_labels.push(item.clone()); + car_to_index.insert(item, car_labels.len()); + car_labels.push(item.to_owned()); } *count += 1; } // Verify each car appears exactly twice for (car, count) in &car_count { - assert_eq!( - *count, 2, - "Each car must appear exactly twice, but '{}' appears {} times", - car, count - ); + if *count != 2 { + return Err(format!( + "each car must appear exactly twice, but '{car}' appears {count} times" + ) + .into()); + } } // Convert sequence to indices - let sequence_indices: Vec = sequence.iter().map(|item| car_to_index[item]).collect(); + let sequence_indices: Vec = sequence + .iter() + .map(|item| car_to_index[item.as_ref()]) + .collect(); // Determine which positions are first occurrences let mut seen: HashSet = HashSet::new(); @@ -107,12 +134,12 @@ impl PaintShop { let num_cars = car_labels.len(); - Self { + Ok(Self { sequence_indices, car_labels, is_first, num_cars, - } + }) } /// Get the sequence length. @@ -214,8 +241,12 @@ impl Problem for PaintShop { } impl crate::solvers::BruteForceProblem for PaintShop { - fn dimensions(&self) -> Vec { - vec![2; self.num_cars] + fn num_variables(&self) -> Result { + Ok(self.num_cars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -231,7 +262,7 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "paintshop", - instance: Box::new(PaintShop::new(vec!["A", "B", "A", "C", "B", "C"])), + instance: Box::new(PaintShop::new(vec!["A", "B", "A", "C", "B", "C"]).unwrap()), optimal_config: serde_json::json!(vec![false, false, true]), optimal_value: serde_json::json!(2), }] diff --git a/src/models/misc/partially_ordered_knapsack.rs b/src/models/misc/partially_ordered_knapsack.rs index af4e40f5b..4d6a912d6 100644 --- a/src/models/misc/partially_ordered_knapsack.rs +++ b/src/models/misc/partially_ordered_knapsack.rs @@ -46,7 +46,7 @@ inventory::submit! { /// vec![3, 2, 5, 4, 3, 8], // values /// vec![(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)], // precedences /// 11, // capacity -/// ); +/// ).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); @@ -84,55 +84,12 @@ impl TryFrom for PartiallyOrderedKnapsack { type Error = crate::registry::ConstructionError; fn try_from(spec: PartiallyOrderedKnapsackCreateSpec) -> Result { - if spec.weights.len() != spec.values.len() { - return Err("weights and values must have the same length" - .to_string() - .into()); - } - if spec.capacity < 0 { - return Err("capacity must be non-negative".to_string().into()); - } - if let Some((index, weight)) = spec - .weights - .iter() - .enumerate() - .find(|(_, weight)| **weight < 0) - { - return Err(format!("weight[{index}] must be non-negative, got {weight}").into()); - } - if let Some((index, value)) = spec - .values - .iter() - .enumerate() - .find(|(_, value)| **value < 0) - { - return Err(format!("value[{index}] must be non-negative, got {value}").into()); - } - let precedences = spec.precedences.unwrap_or_default(); - let num_items = spec.weights.len(); - if let Some(&(pred, succ)) = precedences - .iter() - .find(|&&(pred, succ)| pred >= num_items || succ >= num_items) - { - return Err(format!( - "precedence ({pred}, {succ}) is out of range for {num_items} items" - ) - .into()); - } - let predecessors = Self::compute_predecessors(&precedences, num_items); - if let Some(item) = predecessors - .iter() - .enumerate() - .find_map(|(item, preds)| preds.contains(&item).then_some(item)) - { - return Err(format!("precedences contain a cycle involving item {item}").into()); - } - Ok(Self::new( + Self::new( spec.weights, spec.values, - precedences, + spec.precedences.unwrap_or_default(), spec.capacity, - )) + ) } } @@ -151,12 +108,8 @@ impl Serialize for PartiallyOrderedKnapsack { impl<'de> Deserialize<'de> for PartiallyOrderedKnapsack { fn deserialize>(deserializer: D) -> Result { let raw = PartiallyOrderedKnapsackRaw::deserialize(deserializer)?; - Ok(Self::new( - raw.weights, - raw.values, - raw.precedences, - raw.capacity, - )) + Self::new(raw.weights, raw.values, raw.precedences, raw.capacity) + .map_err(serde::de::Error::custom) } } @@ -169,8 +122,8 @@ impl PartiallyOrderedKnapsack { /// * `precedences` - Precedence pairs `(a, b)` meaning item `a` must be included before item `b` /// * `capacity` - Knapsack capacity C /// - /// # Panics - /// Panics if `weights` and `values` have different lengths, if any weight, + /// # Errors + /// Returns an error if `weights` and `values` have different lengths, if any weight, /// value, or capacity is negative, if any precedence index is out of bounds, /// or if the precedences contain a cycle. pub fn new( @@ -178,39 +131,46 @@ impl PartiallyOrderedKnapsack { values: Vec, precedences: Vec<(usize, usize)>, capacity: i64, - ) -> Self { - assert_eq!( - weights.len(), - values.len(), - "weights and values must have the same length" - ); - assert!(capacity >= 0, "capacity must be non-negative"); + ) -> Result { + if weights.len() != values.len() { + return Err("weights and values must have the same length".into()); + }; + if !(capacity >= 0) { + return Err("capacity must be non-negative".into()); + }; for (i, &w) in weights.iter().enumerate() { - assert!(w >= 0, "weight[{i}] must be non-negative, got {w}"); + if !(w >= 0) { + return Err(format!("weight[{i}] must be non-negative, got {w}").into()); + }; } for (i, &v) in values.iter().enumerate() { - assert!(v >= 0, "value[{i}] must be non-negative, got {v}"); + if !(v >= 0) { + return Err(format!("value[{i}] must be non-negative, got {v}").into()); + }; } let n = weights.len(); for &(a, b) in &precedences { - assert!(a < n, "precedence index {a} out of bounds (n={n})"); - assert!(b < n, "precedence index {b} out of bounds (n={n})"); + if !(a < n) { + return Err(format!("precedence index {a} out of bounds (n={n})").into()); + }; + if !(b < n) { + return Err(format!("precedence index {b} out of bounds (n={n})").into()); + }; } let predecessors = Self::compute_predecessors(&precedences, n); // Check for cycles: if any item is its own transitive predecessor, the DAG has a cycle for (i, preds) in predecessors.iter().enumerate() { - assert!( - !preds.contains(&i), - "precedences contain a cycle involving item {i}" - ); + if !(!preds.contains(&i)) { + return Err(format!("precedences contain a cycle involving item {i}").into()); + }; } - Self { + Ok(Self { weights, values, precedences, capacity, predecessors, - } + }) } /// Compute transitive predecessors for each item via Floyd-Warshall. @@ -344,8 +304,12 @@ impl Problem for PartiallyOrderedKnapsack { } impl crate::solvers::BruteForceProblem for PartiallyOrderedKnapsack { - fn dimensions(&self) -> Vec { - vec![2; self.num_items()] + fn num_variables(&self) -> Result { + Ok(self.num_items()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -361,12 +325,15 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "partially_ordered_knapsack", - instance: Box::new(PartiallyOrderedKnapsack::new( - vec![2, 3, 4, 1, 2, 3], - vec![3, 2, 5, 4, 3, 8], - vec![(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)], - 11, - )), + instance: Box::new( + PartiallyOrderedKnapsack::new( + vec![2, 3, 4, 1, 2, 3], + vec![3, 2, 5, 4, 3, 8], + vec![(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)], + 11, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![true, true, false, true, true, true]), optimal_value: serde_json::json!(20), }] diff --git a/src/models/misc/partition.rs b/src/models/misc/partition.rs index 032f4eccc..dba5f04be 100644 --- a/src/models/misc/partition.rs +++ b/src/models/misc/partition.rs @@ -137,8 +137,12 @@ impl Problem for Partition { } impl crate::solvers::BruteForceProblem for Partition { - fn dimensions(&self) -> Vec { - vec![2; self.num_elements()] + fn num_variables(&self) -> Result { + Ok(self.num_elements()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/precedence_constrained_scheduling.rs b/src/models/misc/precedence_constrained_scheduling.rs index b062b26a2..9fdb95b0f 100644 --- a/src/models/misc/precedence_constrained_scheduling.rs +++ b/src/models/misc/precedence_constrained_scheduling.rs @@ -41,12 +41,13 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // 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 problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "PrecedenceConstrainedSchedulingData")] pub struct PrecedenceConstrainedScheduling { num_tasks: usize, num_processors: usize, @@ -54,6 +55,26 @@ pub struct PrecedenceConstrainedScheduling { precedences: Vec<(usize, usize)>, } +#[derive(Deserialize)] +struct PrecedenceConstrainedSchedulingData { + num_tasks: usize, + num_processors: usize, + deadline: i64, + precedences: Vec<(usize, usize)>, +} + +impl TryFrom for PrecedenceConstrainedScheduling { + type Error = crate::registry::ConstructionError; + fn try_from(data: PrecedenceConstrainedSchedulingData) -> Result { + Self::new( + data.num_tasks, + data.num_processors, + data.deadline, + data.precedences, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct PrecedenceConstrainedSchedulingCreateSpec { num_tasks: usize, @@ -66,80 +87,54 @@ impl TryFrom for PrecedenceConstraine type Error = crate::registry::ConstructionError; fn try_from(spec: PrecedenceConstrainedSchedulingCreateSpec) -> Result { - if spec.num_tasks > 0 && spec.num_processors == 0 { - return Err("num_processors must be positive when there are tasks" - .to_string() - .into()); - } - if spec.num_tasks > 0 && spec.deadline == 0 { - return Err("deadline must be positive when there are tasks" - .to_string() - .into()); - } - if spec.deadline < 0 || usize::try_from(spec.deadline).is_err() { - return Err("deadline must be nonnegative and fit usize" - .to_string() - .into()); - } - let precedences = spec.precedences.unwrap_or_default(); - if let Some(&(pred, succ)) = precedences - .iter() - .find(|&&(pred, succ)| pred >= spec.num_tasks || succ >= spec.num_tasks) - { - return Err(format!( - "precedence ({pred}, {succ}) is out of range for {} tasks", - spec.num_tasks - ) - .into()); - } - Ok(Self::new( + Self::new( spec.num_tasks, spec.num_processors, spec.deadline, - precedences, - )) + spec.precedences.unwrap_or_default(), + ) } } impl PrecedenceConstrainedScheduling { /// Create a new Precedence Constrained Scheduling instance. /// - /// # Panics + /// # Errors /// - /// Panics if `num_processors` or `deadline` is zero (when `num_tasks > 0`), + /// Returns an error if `num_processors` or `deadline` is zero (when `num_tasks > 0`), /// or if any precedence index is out of bounds (>= num_tasks). pub fn new( num_tasks: usize, num_processors: usize, deadline: i64, precedences: Vec<(usize, usize)>, - ) -> Self { + ) -> Result { if num_tasks > 0 { - assert!( - num_processors > 0, - "num_processors must be > 0 when there are tasks" - ); - assert!(deadline > 0, "deadline must be > 0 when there are tasks"); + if num_processors == 0 { + return Err("num_processors must be > 0 when there are tasks".into()); + } + if deadline <= 0 { + return Err("deadline must be > 0 when there are tasks".into()); + } + } + if !(deadline >= 0) { + return Err("deadline must be nonnegative".into()); } - assert!( - deadline >= 0 && usize::try_from(deadline).is_ok(), - "deadline must be nonnegative and fit usize" - ); for &(i, j) in &precedences { - assert!( - i < num_tasks && j < num_tasks, - "Precedence ({}, {}) out of bounds for {} tasks", - i, - j, - num_tasks - ); + if !(i < num_tasks && j < num_tasks) { + return Err(format!( + "Precedence ({}, {}) out of bounds for {} tasks", + i, j, num_tasks + ) + .into()); + } } - Self { + Ok(Self { num_tasks, num_processors, deadline, precedences, - } + }) } /// Get the number of tasks. @@ -194,24 +189,26 @@ impl Problem for PrecedenceConstrainedScheduling { "schedule length does not match the tasks".into(), )); } - let deadline = - usize::try_from(self.deadline).expect("validated deadline must fit usize"); - if config.iter().any(|&v| v >= deadline) { + if config + .iter() + .any(|&v| v as i128 >= i128::from(self.deadline)) + { return Err(crate::traits::EvaluationError::InvalidConfiguration( "schedule contains an out-of-range time slot".into(), )); } // Check processor capacity: at most num_processors tasks per time slot - let mut slot_count = vec![0usize; deadline]; + let mut slot_count = std::collections::BTreeMap::new(); for &slot in config { - slot_count[slot] += 1; - if slot_count[slot] > self.num_processors { + let count = slot_count.entry(slot).or_insert(0usize); + *count += 1; + if *count > self.num_processors { return Ok(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 { + if config[j] <= config[i] { return Ok(crate::types::Or(false)); } } @@ -222,11 +219,12 @@ impl Problem for PrecedenceConstrainedScheduling { } impl crate::solvers::BruteForceProblem for PrecedenceConstrainedScheduling { - fn dimensions(&self) -> Vec { - vec![ - usize::try_from(self.deadline).expect("validated deadline must fit usize"); - self.num_tasks - ] + fn num_variables(&self) -> Result { + Ok(self.num_tasks) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(usize::try_from(self.deadline)?) } } @@ -243,22 +241,25 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec { - let d = self.d_max(); - vec![2; self.num_tasks() * d] + fn num_variables(&self) -> Result { + (self.num_tasks()).checked_mul(self.d_max()).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/production_planning.rs b/src/models/misc/production_planning.rs index 58e0449cc..34db73d8f 100644 --- a/src/models/misc/production_planning.rs +++ b/src/models/misc/production_planning.rs @@ -24,8 +24,8 @@ inventory::submit! { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ProductionPlanningCreateSpec")] pub struct ProductionPlanning { - #[serde(deserialize_with = "positive_usize::deserialize")] num_periods: usize, demands: Vec, capacities: Vec, @@ -55,31 +55,7 @@ struct ProductionPlanningCreateSpec { impl TryFrom for ProductionPlanning { type Error = crate::registry::ConstructionError; fn try_from(spec: ProductionPlanningCreateSpec) -> Result { - if spec.num_periods == 0 { - return Err("num_periods must be positive".to_string().into()); - } - for (name, len) in [ - ("demands", spec.demands.len()), - ("capacities", spec.capacities.len()), - ("setup_costs", spec.setup_costs.len()), - ("production_costs", spec.production_costs.len()), - ("inventory_costs", spec.inventory_costs.len()), - ] { - if len != spec.num_periods { - return Err( - format!("{name} has {len} entries, expected {}", spec.num_periods).into(), - ); - } - } - if spec.capacities.iter().any(|&capacity| { - usize::try_from(capacity) - .ok() - .and_then(|v| v.checked_add(1)) - .is_none() - }) { - return Err("capacities must fit in usize for dims()".to_string().into()); - } - Ok(Self::new( + Self::new( spec.num_periods, spec.demands, spec.capacities, @@ -87,7 +63,7 @@ impl TryFrom for ProductionPlanning { spec.production_costs, spec.inventory_costs, spec.cost_bound, - )) + ) } } @@ -100,8 +76,10 @@ impl ProductionPlanning { production_costs: Vec, inventory_costs: Vec, cost_bound: i64, - ) -> Self { - assert!(num_periods > 0, "num_periods must be positive"); + ) -> Result { + if num_periods == 0 { + return Err("num_periods must be positive".into()); + } for len in [ demands.len(), capacities.len(), @@ -109,33 +87,25 @@ impl ProductionPlanning { production_costs.len(), inventory_costs.len(), ] { - assert_eq!( - len, num_periods, - "all per-period vectors must have length num_periods" - ); + if len != num_periods { + return Err("all per-period vectors must have length num_periods".into()); + } + } + if !(demands + .iter() + .chain(&capacities) + .chain(&setup_costs) + .chain(&production_costs) + .chain(&inventory_costs) + .all(|&value| value >= 0)) + { + return Err("demands, capacities, and costs must be nonnegative".into()); + } + if !(cost_bound >= 0) { + return Err("cost bound must be nonnegative".into()); } - assert!( - capacities.iter().all(|&capacity| { - usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)) - .is_some() - }), - "capacities must fit in usize for dims()" - ); - assert!( - demands - .iter() - .chain(&capacities) - .chain(&setup_costs) - .chain(&production_costs) - .chain(&inventory_costs) - .all(|&value| value >= 0), - "demands, capacities, and costs must be nonnegative" - ); - assert!(cost_bound >= 0, "cost bound must be nonnegative"); - Self { + Ok(Self { num_periods, demands, capacities, @@ -143,7 +113,7 @@ impl ProductionPlanning { production_costs, inventory_costs, cost_bound, - } + }) } pub fn num_periods(&self) -> usize { @@ -200,11 +170,7 @@ impl Problem for ProductionPlanning { let mut total_cost = 0_i64; for (i, &production) in config.iter().enumerate() { - let capacity = match usize::try_from(self.capacities[i]) { - Ok(value) => value, - Err(_) => return Ok(Or(false)), - }; - if production > capacity { + if production as i128 > i128::from(self.capacities[i]) { return Ok(Or(false)); } @@ -289,16 +255,12 @@ impl Problem for ProductionPlanning { } impl crate::solvers::BruteForceProblem for ProductionPlanning { - fn dimensions(&self) -> Vec { - self.capacities - .iter() - .map(|&capacity| { - usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)) - .expect("capacities validated in constructor") - }) - .collect() + fn num_variables(&self) -> Result { + Ok(self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.capacities[variable]) + 1)?) } } @@ -314,36 +276,23 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "production_planning", - instance: Box::new(ProductionPlanning::new( - 4, - vec![2, 1, 3, 2], - vec![4, 4, 4, 4], - vec![2, 2, 2, 2], - vec![1, 1, 1, 1], - vec![1, 1, 1, 1], - 16, - )), + instance: Box::new( + ProductionPlanning::new( + 4, + vec![2, 1, 3, 2], + vec![4, 4, 4, 4], + vec![2, 2, 2, 2], + vec![1, 1, 1, 1], + vec![1, 1, 1, 1], + 16, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![3, 0, 4, 1]), optimal_value: serde_json::json!(true), }] } -mod positive_usize { - use serde::de::Error; - use serde::{Deserialize, Deserializer}; - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = usize::deserialize(deserializer)?; - if value == 0 { - return Err(D::Error::custom("expected positive integer, got 0")); - } - Ok(value) - } -} - #[cfg(test)] #[path = "../../unit_tests/models/misc/production_planning.rs"] mod tests; diff --git a/src/models/misc/rectilinear_picture_compression.rs b/src/models/misc/rectilinear_picture_compression.rs index cd399f591..ef93ef008 100644 --- a/src/models/misc/rectilinear_picture_compression.rs +++ b/src/models/misc/rectilinear_picture_compression.rs @@ -54,7 +54,7 @@ inventory::submit! { /// vec![false, false, true, true], /// vec![false, false, true, true], /// ]; -/// let problem = RectilinearPictureCompression::new(matrix, 2); +/// let problem = RectilinearPictureCompression::new(matrix, 2).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); @@ -78,31 +78,37 @@ impl<'de> Deserialize<'de> for RectilinearPictureCompression { bound: i64, } let inner = Inner::deserialize(deserializer)?; - Ok(Self::new(inner.matrix, inner.bound)) + Self::new(inner.matrix, inner.bound).map_err(serde::de::Error::custom) } } impl RectilinearPictureCompression { /// Create a new RectilinearPictureCompression instance. /// - /// # Panics + /// # Errors /// - /// Panics if `matrix` is empty or has inconsistent row lengths. - pub fn new(matrix: Vec>, bound: i64) -> Self { - assert!(!matrix.is_empty(), "Matrix must not be empty"); + /// Returns an error if `matrix` is empty or has inconsistent row lengths. + pub fn new( + matrix: Vec>, + bound: i64, + ) -> Result { + if matrix.is_empty() { + return Err("Matrix must not be empty".into()); + }; let cols = matrix[0].len(); - assert!(cols > 0, "Matrix must have at least one column"); - assert!( - matrix.iter().all(|row| row.len() == cols), - "All rows must have the same length" - ); + if !(cols > 0) { + return Err("Matrix must have at least one column".into()); + }; + if !(matrix.iter().all(|row| row.len() == cols)) { + return Err("All rows must have the same length".into()); + }; let mut instance = Self { matrix, bound, maximal_rects: Vec::new(), }; instance.maximal_rects = instance.compute_maximal_rectangles(); - instance + Ok(instance) } /// Returns the number of rows in the matrix. @@ -297,8 +303,12 @@ impl Problem for RectilinearPictureCompression { } impl crate::solvers::BruteForceProblem for RectilinearPictureCompression { - fn dimensions(&self) -> Vec { - vec![2; self.maximal_rects.len()] + fn num_variables(&self) -> Result { + Ok(self.maximal_rects.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -317,15 +327,18 @@ pub(crate) fn canonical_model_example_specs() -> Vec, + bound: usize, +} + +impl TryFrom for RegisterSufficiency { + type Error = crate::registry::ConstructionError; + + fn try_from(data: RegisterSufficiencyData) -> Result { + Self::new(data.num_vertices, data.arcs, data.bound) + } +} + impl RegisterSufficiency { /// Create a new Register Sufficiency instance. /// - /// # Panics + /// # Errors /// - /// Panics if any arc index is out of bounds (>= num_vertices), + /// Returns an error if any arc index is out of bounds (>= num_vertices), /// or if any arc is a self-loop. - pub fn new(num_vertices: usize, arcs: Vec<(usize, usize)>, bound: usize) -> Self { + pub fn new( + num_vertices: usize, + arcs: Vec<(usize, usize)>, + bound: usize, + ) -> Result { for &(v, u) in &arcs { - assert!( - v < num_vertices && u < num_vertices, - "Arc ({}, {}) out of bounds for {} vertices", - v, - u, - num_vertices - ); - assert!(v != u, "Self-loop ({}, {}) not allowed in a DAG", v, u); + if !(v < num_vertices && u < num_vertices) { + return Err(format!( + "Arc ({}, {}) out of bounds for {} vertices", + v, u, num_vertices + ) + .into()); + } + if v == u { + return Err(format!("Self-loop ({}, {}) not allowed in a DAG", v, u).into()); + } } - Self { + Ok(Self { num_vertices, arcs, bound, - } + }) } /// Get the number of vertices. @@ -391,8 +413,12 @@ impl Problem for RegisterSufficiency { } impl crate::solvers::BruteForceProblem for RegisterSufficiency { - fn dimensions(&self) -> Vec { - vec![self.num_vertices; self.num_vertices] + fn num_variables(&self) -> Result { + Ok(self.num_vertices) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_vertices) } } @@ -411,20 +437,23 @@ pub(crate) fn canonical_model_example_specs() -> Vec positions [0,1,2,3,5,4,6] - instance: Box::new(RegisterSufficiency::new( - 7, - vec![ - (2, 0), - (2, 1), - (3, 1), - (4, 2), - (4, 3), - (5, 0), - (6, 4), - (6, 5), - ], - 3, - )), + instance: Box::new( + RegisterSufficiency::new( + 7, + vec![ + (2, 0), + (2, 1), + (3, 1), + (4, 2), + (4, 3), + (5, 0), + (6, 4), + (6, 5), + ], + 3, + ) + .unwrap(), + ), // Order: v1,v2,v3,v4,v6,v5,v7 (1-indexed) = v0,v1,v2,v3,v5,v4,v6 (0-indexed) // Positions: v0->0, v1->1, v2->2, v3->3, v4->5, v5->4, v6->6 optimal_config: serde_json::json!(vec![0, 1, 2, 3, 5, 4, 6]), diff --git a/src/models/misc/resource_constrained_scheduling.rs b/src/models/misc/resource_constrained_scheduling.rs index c43d4bdca..1e9a2d205 100644 --- a/src/models/misc/resource_constrained_scheduling.rs +++ b/src/models/misc/resource_constrained_scheduling.rs @@ -254,8 +254,12 @@ impl Problem for ResourceConstrainedScheduling { } impl crate::solvers::BruteForceProblem for ResourceConstrainedScheduling { - fn dimensions(&self) -> Vec { - vec![self.deadline as usize; self.num_tasks()] + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(usize::try_from(self.deadline)?) } } diff --git a/src/models/misc/scheduling_to_minimize_weighted_completion_time.rs b/src/models/misc/scheduling_to_minimize_weighted_completion_time.rs index 68459fc3f..67a83a47a 100644 --- a/src/models/misc/scheduling_to_minimize_weighted_completion_time.rs +++ b/src/models/misc/scheduling_to_minimize_weighted_completion_time.rs @@ -37,7 +37,7 @@ inventory::submit! { /// # Representation /// /// Each task has a variable in `{0, ..., m-1}` representing its processor -/// assignment, giving `dims() = [m; n]`. +/// assignment, giving `coordinate cardinalities = [m; n]`. /// /// # Example /// @@ -303,8 +303,12 @@ impl Problem for SchedulingToMinimizeWeightedCompletionTime { } impl crate::solvers::BruteForceProblem for SchedulingToMinimizeWeightedCompletionTime { - fn dimensions(&self) -> Vec { - vec![self.num_processors; self.num_tasks()] + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_processors) } } diff --git a/src/models/misc/scheduling_with_individual_deadlines.rs b/src/models/misc/scheduling_with_individual_deadlines.rs index 6f8b21658..6f0761e04 100644 --- a/src/models/misc/scheduling_with_individual_deadlines.rs +++ b/src/models/misc/scheduling_with_individual_deadlines.rs @@ -29,6 +29,7 @@ inventory::submit! { /// satisfies `sigma(u) + 1 <= sigma(v)` and no time slot hosts more than /// `num_processors` tasks. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "SchedulingWithIndividualDeadlinesData")] pub struct SchedulingWithIndividualDeadlines { num_tasks: usize, num_processors: usize, @@ -36,6 +37,26 @@ pub struct SchedulingWithIndividualDeadlines { precedences: Vec<(usize, usize)>, } +#[derive(Deserialize)] +struct SchedulingWithIndividualDeadlinesData { + num_tasks: usize, + num_processors: usize, + deadlines: Vec, + precedences: Vec<(usize, usize)>, +} + +impl TryFrom for SchedulingWithIndividualDeadlines { + type Error = crate::registry::ConstructionError; + fn try_from(data: SchedulingWithIndividualDeadlinesData) -> Result { + Self::new( + data.num_tasks, + data.num_processors, + data.deadlines, + data.precedences, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct SchedulingWithIndividualDeadlinesCreateSpec { /// Number of tasks. @@ -50,43 +71,12 @@ struct SchedulingWithIndividualDeadlinesCreateSpec { impl TryFrom for SchedulingWithIndividualDeadlines { type Error = crate::registry::ConstructionError; fn try_from(spec: SchedulingWithIndividualDeadlinesCreateSpec) -> Result { - if spec.deadlines.len() != spec.num_tasks { - return Err(format!( - "deadlines has {} entries, expected {}", - spec.deadlines.len(), - spec.num_tasks - ) - .into()); - } - if spec.deadlines.iter().any(|&deadline| deadline < 0) { - return Err("deadlines must be nonnegative".to_string().into()); - } - if spec - .deadlines - .iter() - .any(|&deadline| usize::try_from(deadline).is_err()) - { - return Err("deadlines must fit usize to define schedule slots" - .to_string() - .into()); - } - let precedences = spec.precedences.unwrap_or_default(); - if let Some(&(pred, succ)) = precedences - .iter() - .find(|&&(p, s)| p >= spec.num_tasks || s >= spec.num_tasks) - { - return Err(format!( - "precedence ({pred}, {succ}) is out of range for {} tasks", - spec.num_tasks - ) - .into()); - } - Ok(Self::new( + Self::new( spec.num_tasks, spec.num_processors, spec.deadlines, - precedences, - )) + spec.precedences.unwrap_or_default(), + ) } } @@ -96,43 +86,36 @@ impl SchedulingWithIndividualDeadlines { num_processors: usize, deadlines: Vec, precedences: Vec<(usize, usize)>, - ) -> Self { - assert_eq!( - deadlines.len(), - num_tasks, - "deadlines length must equal num_tasks" - ); - assert!( - deadlines.iter().all(|&deadline| deadline >= 0), - "deadlines must be nonnegative" - ); - assert!( - deadlines - .iter() - .all(|&deadline| usize::try_from(deadline).is_ok()), - "deadlines must fit usize to define schedule slots" - ); + ) -> Result { + if deadlines.len() != num_tasks { + return Err("deadlines length must equal num_tasks".into()); + } + if !(deadlines.iter().all(|&deadline| deadline >= 0)) { + return Err("deadlines must be nonnegative".into()); + } for &(pred, succ) in &precedences { - assert!( - pred < num_tasks, - "predecessor index {} out of range (num_tasks = {})", - pred, - num_tasks - ); - assert!( - succ < num_tasks, - "successor index {} out of range (num_tasks = {})", - succ, - num_tasks - ); + if !(pred < num_tasks) { + return Err(format!( + "predecessor index {} out of range (num_tasks = {})", + pred, num_tasks + ) + .into()); + } + if !(succ < num_tasks) { + return Err(format!( + "successor index {} out of range (num_tasks = {})", + succ, num_tasks + ) + .into()); + } } - Self { + Ok(Self { num_tasks, num_processors, deadlines, precedences, - } + }) } pub fn num_tasks(&self) -> usize { @@ -188,15 +171,13 @@ impl Problem for SchedulingWithIndividualDeadlines { } for (&start, &deadline) in config.iter().zip(&self.deadlines) { - let deadline = - usize::try_from(deadline).expect("validated deadline must fit usize"); - if start >= deadline { + if start as i128 >= i128::from(deadline) { return Ok(crate::types::Or(false)); } } for &(pred, succ) in &self.precedences { - if config[pred] + 1 > config[succ] { + if config[pred] >= config[succ] { return Ok(crate::types::Or(false)); } } @@ -217,11 +198,12 @@ impl Problem for SchedulingWithIndividualDeadlines { } impl crate::solvers::BruteForceProblem for SchedulingWithIndividualDeadlines { - fn dimensions(&self) -> Vec { - self.deadlines - .iter() - .map(|&deadline| usize::try_from(deadline).expect("validated deadline must fit usize")) - .collect() + fn num_variables(&self) -> Result { + Ok(self.deadlines.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from(self.deadlines[variable])?) } } @@ -237,12 +219,15 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "scheduling_with_individual_deadlines", - instance: Box::new(SchedulingWithIndividualDeadlines::new( - 7, - 3, - vec![2, 1, 2, 2, 3, 3, 2], - vec![(0, 3), (1, 3), (1, 4), (2, 4), (2, 5)], - )), + instance: Box::new( + SchedulingWithIndividualDeadlines::new( + 7, + 3, + vec![2, 1, 2, 2, 3, 3, 2], + vec![(0, 3), (1, 3), (1, 4), (2, 4), (2, 5)], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![0, 0, 0, 1, 2, 1, 1]), optimal_value: serde_json::json!(true), }] 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 95b9cc819..47c71350e 100644 --- a/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs +++ b/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs @@ -209,8 +209,12 @@ impl Problem for SequencingToMinimizeMaximumCumulativeCost { } impl crate::solvers::BruteForceProblem for SequencingToMinimizeMaximumCumulativeCost { - fn dimensions(&self) -> Vec { - super::lehmer_dims(self.num_tasks()) + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.num_tasks() - variable) } } diff --git a/src/models/misc/sequencing_to_minimize_tardy_task_weight.rs b/src/models/misc/sequencing_to_minimize_tardy_task_weight.rs index c60474b40..56119e70a 100644 --- a/src/models/misc/sequencing_to_minimize_tardy_task_weight.rs +++ b/src/models/misc/sequencing_to_minimize_tardy_task_weight.rs @@ -31,7 +31,7 @@ inventory::submit! { /// This is the weighted generalization of minimizing the number of tardy tasks /// (problem SS8 in Garey & Johnson, 1979, written $1 || sum w_j U_j$). /// -/// Configurations are direct permutation encodings with `dims() = [n; n]`: +/// Configurations are direct permutation encodings with `coordinate cardinalities = [n; n]`: /// each position holds the index of the task scheduled at that position. /// A configuration is valid iff it is a permutation of `0..n`. #[derive(Debug, Clone, Serialize)] @@ -221,9 +221,12 @@ impl Problem for SequencingToMinimizeTardyTaskWeight { } impl crate::solvers::BruteForceProblem for SequencingToMinimizeTardyTaskWeight { - fn dimensions(&self) -> Vec { - let n = self.num_tasks(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_tasks()) } } 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 05cd17d62..eb8143890 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs @@ -35,7 +35,7 @@ inventory::submit! { /// and minimizes `sum_t w(t) * C(t)`, where `C(t)` is the completion time of /// task `t`. /// -/// Configurations use Lehmer code with `dims() = [n, n-1, ..., 1]`. +/// Configurations use Lehmer code with `coordinate cardinalities = [n, n-1, ..., 1]`. #[derive(Debug, Clone, Serialize)] pub struct SequencingToMinimizeWeightedCompletionTime { lengths: Vec, @@ -256,8 +256,12 @@ impl Problem for SequencingToMinimizeWeightedCompletionTime { } impl crate::solvers::BruteForceProblem for SequencingToMinimizeWeightedCompletionTime { - fn dimensions(&self) -> Vec { - super::lehmer_dims(self.num_tasks()) + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.num_tasks() - variable) } } diff --git a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs index 08b6d8419..0e31f9d72 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs @@ -46,12 +46,13 @@ inventory::submit! { /// vec![2, 3, 1, 4, 2], /// vec![5, 8, 4, 15, 10], /// 13, -/// ); +/// ).unwrap(); /// /// let solver = BruteForce::new(); /// assert!(solver.solve(&problem).unwrap().is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "SequencingToMinimizeWeightedTardinessCreateSpec")] pub struct SequencingToMinimizeWeightedTardiness { lengths: Vec, weights: Vec, @@ -77,61 +78,46 @@ impl TryFrom fn try_from( spec: SequencingToMinimizeWeightedTardinessCreateSpec, ) -> Result { - if spec.lengths.len() != spec.weights.len() { - return Err("weights length must equal lengths length" - .to_string() - .into()); - } - if spec.lengths.len() != spec.deadlines.len() { - return Err("deadlines length must equal lengths length" - .to_string() - .into()); - } - Ok(Self::new( - spec.lengths, - spec.weights, - spec.deadlines, - spec.bound, - )) + Self::new(spec.lengths, spec.weights, spec.deadlines, spec.bound) } } impl SequencingToMinimizeWeightedTardiness { /// Create a new weighted tardiness scheduling instance. /// - /// # Panics + /// # Errors /// - /// Panics if the input vectors do not have the same length. - pub fn new(lengths: Vec, weights: Vec, deadlines: Vec, bound: i64) -> Self { - assert_eq!( - lengths.len(), - weights.len(), - "weights length must equal lengths length" - ); - assert_eq!( - lengths.len(), - deadlines.len(), - "deadlines length must equal lengths length" - ); - assert!( - lengths.iter().all(|&length| length >= 0), - "task lengths must be nonnegative" - ); - assert!( - weights.iter().all(|&weight| weight >= 0), - "task weights must be nonnegative" - ); - assert!( - deadlines.iter().all(|&deadline| deadline >= 0), - "deadlines must be nonnegative" - ); - assert!(bound >= 0, "bound must be nonnegative"); - Self { + /// Returns an error if the input vectors do not have the same length. + pub fn new( + lengths: Vec, + weights: Vec, + deadlines: Vec, + bound: i64, + ) -> Result { + if lengths.len() != weights.len() { + return Err("weights length must equal lengths length".into()); + } + if lengths.len() != deadlines.len() { + return Err("deadlines length must equal lengths length".into()); + } + if !(lengths.iter().all(|&length| length >= 0)) { + return Err("task lengths must be nonnegative".into()); + } + if !(weights.iter().all(|&weight| weight >= 0)) { + return Err("task weights must be nonnegative".into()); + } + if !(deadlines.iter().all(|&deadline| deadline >= 0)) { + return Err("deadlines must be nonnegative".into()); + } + if !(bound >= 0) { + return Err("bound must be nonnegative".into()); + } + Ok(Self { lengths, weights, deadlines, bound, - } + }) } /// Returns the job lengths. @@ -249,8 +235,12 @@ impl Problem for SequencingToMinimizeWeightedTardiness { } impl crate::solvers::BruteForceProblem for SequencingToMinimizeWeightedTardiness { - fn dimensions(&self) -> Vec { - super::lehmer_dims(self.num_tasks()) + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.num_tasks() - variable) } } @@ -266,12 +256,15 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "sequencing_to_minimize_weighted_tardiness", - instance: Box::new(SequencingToMinimizeWeightedTardiness::new( - vec![3, 4, 2, 5, 3], - vec![2, 3, 1, 4, 2], - vec![5, 8, 4, 15, 10], - 13, - )), + instance: Box::new( + SequencingToMinimizeWeightedTardiness::new( + vec![3, 4, 2, 5, 3], + vec![2, 3, 1, 4, 2], + vec![5, 8, 4, 15, 10], + 13, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![0, 1, 4, 3, 2]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/misc/sequencing_with_deadlines_and_set_up_times.rs b/src/models/misc/sequencing_with_deadlines_and_set_up_times.rs index 568c4f3df..e1e56be82 100644 --- a/src/models/misc/sequencing_with_deadlines_and_set_up_times.rs +++ b/src/models/misc/sequencing_with_deadlines_and_set_up_times.rs @@ -39,7 +39,7 @@ inventory::submit! { /// This is problem SS14 in Garey & Johnson (1979), written /// $1 | s_{ij} | \text{feasibility}$. /// -/// Configurations are direct permutation encodings with `dims() = [n; n]`: +/// Configurations are direct permutation encodings with `coordinate cardinalities = [n; n]`: /// each position holds the index of the task scheduled at that position. /// A configuration is valid iff it is a permutation of `0..n`. #[derive(Debug, Clone, Serialize)] @@ -239,9 +239,12 @@ impl Problem for SequencingWithDeadlinesAndSetUpTimes { } impl crate::solvers::BruteForceProblem for SequencingWithDeadlinesAndSetUpTimes { - fn dimensions(&self) -> Vec { - let n = self.num_tasks(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_tasks()) } } 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 3bc6449ea..ba5eac69c 100644 --- a/src/models/misc/sequencing_with_release_times_and_deadlines.rs +++ b/src/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -37,7 +37,7 @@ inventory::submit! { /// /// Uses a permutation encoding (Lehmer code), where `config[i]` selects which /// remaining task to schedule next from the pool of unscheduled tasks. -/// `dims() = [n, n-1, ..., 2, 1]`. Tasks are scheduled left-to-right: each +/// `coordinate cardinalities = [n, n-1, ..., 2, 1]`. Tasks are scheduled left-to-right: each /// task starts at `max(release_time, current_time)`. The schedule is feasible /// iff every task finishes by its deadline. /// @@ -51,44 +51,66 @@ inventory::submit! { /// vec![1, 2, 1], /// vec![0, 0, 2], /// vec![3, 3, 4], -/// ); +/// ).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "SequencingWithReleaseTimesAndDeadlinesData")] pub struct SequencingWithReleaseTimesAndDeadlines { lengths: Vec, release_times: Vec, deadlines: Vec, } +#[derive(Deserialize)] +struct SequencingWithReleaseTimesAndDeadlinesData { + lengths: Vec, + release_times: Vec, + deadlines: Vec, +} + +impl TryFrom + for SequencingWithReleaseTimesAndDeadlines +{ + type Error = crate::registry::ConstructionError; + fn try_from(data: SequencingWithReleaseTimesAndDeadlinesData) -> Result { + Self::new(data.lengths, data.release_times, data.deadlines) + } +} + impl SequencingWithReleaseTimesAndDeadlines { /// Create a new instance. /// - /// # Panics + /// # Errors /// - /// Panics if the three vectors have different lengths. - pub fn new(lengths: Vec, release_times: Vec, deadlines: Vec) -> Self { - assert_eq!(lengths.len(), release_times.len()); - assert_eq!(lengths.len(), deadlines.len()); - assert!( - lengths.iter().all(|&length| length >= 0), - "task lengths must be nonnegative" - ); - assert!( - release_times.iter().all(|&release| release >= 0), - "release times must be nonnegative" - ); - assert!( - deadlines.iter().all(|&deadline| deadline >= 0), - "deadlines must be nonnegative" - ); - Self { + /// Returns an error if the three vectors have different lengths. + pub fn new( + lengths: Vec, + release_times: Vec, + deadlines: Vec, + ) -> Result { + if lengths.len() != release_times.len() { + return Err("lengths and release_times must have the same length".into()); + } + if lengths.len() != deadlines.len() { + return Err("lengths and deadlines must have the same length".into()); + } + if !(lengths.iter().all(|&length| length >= 0)) { + return Err("task lengths must be nonnegative".into()); + } + if !(release_times.iter().all(|&release| release >= 0)) { + return Err("release times must be nonnegative".into()); + } + if !(deadlines.iter().all(|&deadline| deadline >= 0)) { + return Err("deadlines must be nonnegative".into()); + } + Ok(Self { lengths, release_times, deadlines, - } + }) } /// Returns the processing times. @@ -167,8 +189,12 @@ impl Problem for SequencingWithReleaseTimesAndDeadlines { } impl crate::solvers::BruteForceProblem for SequencingWithReleaseTimesAndDeadlines { - fn dimensions(&self) -> Vec { - super::lehmer_dims(self.num_tasks()) + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.num_tasks() - variable) } } @@ -187,11 +213,14 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec { - self.start_slot_counts().collect() + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self + .start_slot_counts() + .nth(variable) + .expect("coordinate index is in range")) } } diff --git a/src/models/misc/shortest_common_supersequence.rs b/src/models/misc/shortest_common_supersequence.rs index b158a5347..4b4055f1d 100644 --- a/src/models/misc/shortest_common_supersequence.rs +++ b/src/models/misc/shortest_common_supersequence.rs @@ -49,18 +49,33 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // Alphabet {0, 1}, strings [0,1] and [1,0] -/// let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]]); +/// let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]]).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ShortestCommonSupersequenceData")] pub struct ShortestCommonSupersequence { alphabet_size: usize, strings: Vec>, max_length: usize, } +#[derive(Deserialize)] +struct ShortestCommonSupersequenceData { + alphabet_size: usize, + strings: Vec>, +} + +impl TryFrom for ShortestCommonSupersequence { + type Error = crate::registry::ConstructionError; + + fn try_from(data: ShortestCommonSupersequenceData) -> Result { + Self::new(data.alphabet_size, data.strings) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct ShortestCommonSupersequenceCreateSpec { /// Input strings; the alphabet and maximum length are inferred from them. @@ -72,10 +87,6 @@ impl TryFrom for ShortestCommonSuperseque type Error = crate::registry::ConstructionError; fn try_from(spec: ShortestCommonSupersequenceCreateSpec) -> Result { - if spec.strings.is_empty() { - return Err("must have at least one string".to_string().into()); - } - let alphabet_size = spec .strings .iter() @@ -89,17 +100,7 @@ impl TryFrom for ShortestCommonSuperseque }) .transpose()? .unwrap_or(0); - let max_length = spec.strings.iter().try_fold(0_usize, |total, string| { - total - .checked_add(string.len()) - .ok_or_else(|| "maximum supersequence length overflows usize".to_string()) - })?; - - Ok(Self { - alphabet_size, - strings: spec.strings, - max_length, - }) + Self::new(alphabet_size, spec.strings) } } @@ -109,22 +110,30 @@ impl ShortestCommonSupersequence { /// `max_length` is computed automatically as the sum of all input string /// lengths (the worst-case supersequence with no overlap). /// - /// # Panics + /// # Errors /// - /// Panics if `strings` is empty, or if `alphabet_size` is 0 and any input + /// Returns an error if `strings` is empty, or if `alphabet_size` is 0 and any input /// string is non-empty. - pub fn new(alphabet_size: usize, strings: Vec>) -> Self { - assert!(!strings.is_empty(), "must have at least one string"); - let max_length: usize = strings.iter().map(|s| s.len()).sum(); - assert!( - alphabet_size > 0 || strings.iter().all(|s| s.is_empty()), - "alphabet_size must be > 0 when any input string is non-empty" - ); - Self { + pub fn new( + alphabet_size: usize, + strings: Vec>, + ) -> Result { + if strings.is_empty() { + return Err("must have at least one string".into()); + } + let max_length = strings.iter().try_fold(0usize, |total, string| { + total + .checked_add(string.len()) + .ok_or("maximum string length overflows usize") + })?; + if !(alphabet_size > 0 || strings.iter().all(|s| s.is_empty())) { + return Err("alphabet_size must be > 0 when any input string is non-empty".into()); + } + Ok(Self { alphabet_size, strings, max_length, - } + }) } /// Returns the alphabet size. @@ -238,8 +247,14 @@ impl Problem for ShortestCommonSupersequence { } impl crate::solvers::BruteForceProblem for ShortestCommonSupersequence { - fn dimensions(&self) -> Vec { - vec![self.alphabet_size + 1; self.max_length] + fn num_variables(&self) -> Result { + Ok(self.max_length) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.alphabet_size).checked_add(1usize).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } @@ -258,10 +273,9 @@ pub(crate) fn canonical_model_example_specs() -> Vec>, max_length: usize, } +#[derive(Deserialize)] +struct ShortestCommonSuperstringData { + alphabet_size: usize, + strings: Vec>, +} + +impl TryFrom for ShortestCommonSuperstring { + type Error = crate::registry::ConstructionError; + + fn try_from(data: ShortestCommonSuperstringData) -> Result { + Self::new(data.alphabet_size, data.strings) + } +} + impl ShortestCommonSuperstring { /// Create a new ShortestCommonSuperstring instance. /// /// `max_length` is computed automatically as the sum of all input string /// lengths (the trivial upper bound: concatenation with no overlap). /// - /// # Panics + /// # Errors /// - /// Panics if `strings` is empty, or if `alphabet_size` is 0 and any input + /// Returns an error if `strings` is empty, or if `alphabet_size` is 0 and any input /// string is non-empty. - pub fn new(alphabet_size: usize, strings: Vec>) -> Self { - assert!(!strings.is_empty(), "must have at least one string"); - let max_length: usize = strings.iter().map(|s| s.len()).sum(); - assert!( - alphabet_size > 0 || strings.iter().all(|s| s.is_empty()), - "alphabet_size must be > 0 when any input string is non-empty" - ); - Self { + pub fn new( + alphabet_size: usize, + strings: Vec>, + ) -> Result { + if strings.is_empty() { + return Err("must have at least one string".into()); + } + let max_length = strings.iter().try_fold(0usize, |total, string| { + total + .checked_add(string.len()) + .ok_or("maximum string length overflows usize") + })?; + if !(alphabet_size > 0 || strings.iter().all(|s| s.is_empty())) { + return Err("alphabet_size must be > 0 when any input string is non-empty".into()); + } + Ok(Self { alphabet_size, strings, max_length, - } + }) } /// Returns the alphabet size. @@ -202,8 +225,14 @@ impl Problem for ShortestCommonSuperstring { } impl crate::solvers::BruteForceProblem for ShortestCommonSuperstring { - fn dimensions(&self) -> Vec { - vec![self.alphabet_size + 1; self.max_length] + fn num_variables(&self) -> Result { + Ok(self.max_length) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.alphabet_size).checked_add(1usize).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } @@ -223,10 +252,9 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec { - vec![self.tiles.len(); self.grid_size * self.grid_size] + fn num_variables(&self) -> Result { + (self.grid_size).checked_mul(self.grid_size).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.tiles.len()) } } diff --git a/src/models/misc/stacker_crane.rs b/src/models/misc/stacker_crane.rs index 6d56ca1b8..a04445f40 100644 --- a/src/models/misc/stacker_crane.rs +++ b/src/models/misc/stacker_crane.rs @@ -368,8 +368,12 @@ impl Problem for StackerCrane { } impl crate::solvers::BruteForceProblem for StackerCrane { - fn dimensions(&self) -> Vec { - vec![self.num_arcs(); self.num_arcs()] + fn num_variables(&self) -> Result { + Ok(self.num_arcs()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_arcs()) } } diff --git a/src/models/misc/staff_scheduling.rs b/src/models/misc/staff_scheduling.rs index 1d5d60a0d..803c310b2 100644 --- a/src/models/misc/staff_scheduling.rs +++ b/src/models/misc/staff_scheduling.rs @@ -27,6 +27,7 @@ inventory::submit! { /// pattern. A configuration is satisfying iff the total assigned workers does /// not exceed `num_workers` and every period's staffing requirement is met. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "StaffSchedulingData")] pub struct StaffScheduling { shifts_per_schedule: usize, schedules: Vec>, @@ -34,6 +35,27 @@ pub struct StaffScheduling { num_workers: i64, } +#[derive(Deserialize)] +struct StaffSchedulingData { + shifts_per_schedule: usize, + schedules: Vec>, + requirements: Vec, + num_workers: i64, +} + +impl TryFrom for StaffScheduling { + type Error = crate::registry::ConstructionError; + + fn try_from(data: StaffSchedulingData) -> Result { + Self::new( + data.shifts_per_schedule, + data.schedules, + data.requirements, + data.num_workers, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct StaffSchedulingCreateSpec { /// Required number of active periods in each schedule pattern. @@ -50,48 +72,16 @@ impl TryFrom for StaffScheduling { type Error = crate::registry::ConstructionError; fn try_from(spec: StaffSchedulingCreateSpec) -> Result { - if usize::try_from(spec.num_workers) - .ok() - .and_then(|workers| workers.checked_add(1)) - .is_none() - { - return Err("num_workers must be nonnegative and encodable by dims()" - .to_string() - .into()); - } - for (schedule_index, schedule) in spec.schedules.iter().enumerate() { - if schedule.len() != spec.requirements.len() { - return Err(format!( - "schedules[{schedule_index}] has {} periods, expected {}", - schedule.len(), - spec.requirements.len() - ) - .into()); - } - let active_periods = schedule.iter().filter(|&&active| active).count(); - if active_periods != spec.k { - return Err(format!( - "schedules[{schedule_index}] has {active_periods} active periods, expected {}", - spec.k - ) - .into()); - } - } - Ok(Self::new( - spec.k, - spec.schedules, - spec.requirements, - spec.num_workers, - )) + Self::new(spec.k, spec.schedules, spec.requirements, spec.num_workers) } } impl StaffScheduling { /// Create a new Staff Scheduling instance. /// - /// # Panics + /// # Errors /// - /// Panics if `num_workers` does not fit in `usize`, if any schedule has a + /// Returns an error if `num_workers` does not fit in `usize`, if any schedule has a /// different number of periods than `requirements.len()`, or if any /// schedule has a number of active periods different from /// `shifts_per_schedule`. @@ -100,39 +90,38 @@ impl StaffScheduling { schedules: Vec>, requirements: Vec, num_workers: i64, - ) -> Self { - assert!( - usize::try_from(num_workers) - .ok() - .and_then(|workers| workers.checked_add(1)) - .is_some(), - "num_workers must be nonnegative and encodable by dims()" - ); + ) -> Result { + if !(num_workers >= 0) { + return Err("num_workers must be nonnegative".into()); + } let num_periods = requirements.len(); for (index, schedule) in schedules.iter().enumerate() { - assert_eq!( - schedule.len(), - num_periods, - "schedule {} has {} periods, expected {}", - index, - schedule.len(), - num_periods - ); + if schedule.len() != num_periods { + return Err(format!( + "schedule {} has {} periods, expected {}", + index, + schedule.len(), + num_periods + ) + .into()); + } let ones = schedule.iter().filter(|&&active| active).count(); - assert_eq!( - ones, shifts_per_schedule, - "schedule {} has {} active periods, expected {}", - index, ones, shifts_per_schedule - ); + if ones != shifts_per_schedule { + return Err(format!( + "schedule {} has {} active periods, expected {}", + index, ones, shifts_per_schedule + ) + .into()); + } } - Self { + Ok(Self { shifts_per_schedule, schedules, requirements, num_workers, - } + }) } /// Get the number of periods. @@ -165,13 +154,10 @@ impl StaffScheduling { self.schedules.len() } - fn worker_limit(&self) -> usize { - usize::try_from(self.num_workers) - .expect("validated nonnegative worker count must fit usize") - } - fn worker_counts_valid(&self, config: &[usize]) -> bool { - config.iter().all(|&count| count <= self.worker_limit()) + config + .iter() + .all(|&count| count as i128 <= i128::from(self.num_workers)) } fn within_budget(&self, config: &[usize]) -> Result { @@ -255,8 +241,12 @@ impl Problem for StaffScheduling { } impl crate::solvers::BruteForceProblem for StaffScheduling { - fn dimensions(&self) -> Vec { - vec![self.worker_limit() + 1; self.num_schedules()] + fn num_variables(&self) -> Result { + Ok(self.num_schedules()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.num_workers) + 1)?) } } @@ -272,18 +262,21 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "staff_scheduling", - instance: Box::new(StaffScheduling::new( - 5, - vec![ - vec![true, true, true, true, true, false, false], - vec![false, true, true, true, true, true, false], - vec![false, false, true, true, true, true, true], - vec![true, false, false, true, true, true, true], - vec![true, true, false, false, true, true, true], - ], - vec![2, 2, 2, 3, 3, 2, 1], - 4, - )), + instance: Box::new( + StaffScheduling::new( + 5, + vec![ + vec![true, true, true, true, true, false, false], + vec![false, true, true, true, true, true, false], + vec![false, false, true, true, true, true, true], + vec![true, false, false, true, true, true, true], + vec![true, true, false, false, true, true, true], + ], + vec![2, 2, 2, 3, 3, 2, 1], + 4, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![1, 1, 1, 1, 0]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/misc/string_to_string_correction.rs b/src/models/misc/string_to_string_correction.rs index 0343806e8..26f351501 100644 --- a/src/models/misc/string_to_string_correction.rs +++ b/src/models/misc/string_to_string_correction.rs @@ -60,12 +60,13 @@ inventory::submit! { /// use problemreductions::{Problem, BruteForce}; /// /// // 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 problem = StringToStringCorrection::new(4, vec![0,1,2,3,1,0], vec![0,1,3,2,1], 2).unwrap(); /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "StringToStringCorrectionData")] pub struct StringToStringCorrection { alphabet_size: usize, source: Vec, @@ -73,6 +74,22 @@ pub struct StringToStringCorrection { bound: usize, } +#[derive(Deserialize)] +struct StringToStringCorrectionData { + alphabet_size: usize, + source: Vec, + target: Vec, + bound: usize, +} + +impl TryFrom for StringToStringCorrection { + type Error = crate::registry::ConstructionError; + + fn try_from(data: StringToStringCorrectionData) -> Result { + Self::new(data.alphabet_size, data.source, data.target, data.bound) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct StringToStringCorrectionCreateSpec { /// Optional alphabet size; omitted values are inferred from both strings. @@ -105,52 +122,44 @@ impl TryFrom for StringToStringCorrection { .transpose()? .unwrap_or(0); let alphabet_size = spec.alphabet_size.unwrap_or(inferred_alphabet_size); - if alphabet_size < inferred_alphabet_size { - return Err(format!( - "alphabet size {alphabet_size} is smaller than inferred alphabet size {inferred_alphabet_size}" - ).into()); - } - if alphabet_size == 0 && (!spec.source_string.is_empty() || !spec.target_string.is_empty()) - { - return Err("alphabet size must be positive when either string is non-empty".into()); - } - - Ok(Self { + Self::new( alphabet_size, - source: spec.source_string, - target: spec.target_string, - bound: spec.bound, - }) + spec.source_string, + spec.target_string, + spec.bound, + ) } } impl StringToStringCorrection { /// Create a new StringToStringCorrection instance. /// - /// # Panics + /// # Errors /// - /// Panics if `alphabet_size` is 0 when the source or target string is + /// Returns an error if `alphabet_size` is 0 when the source or target string is /// non-empty, or if any symbol in `source` or `target` is /// `>= alphabet_size`. - pub fn new(alphabet_size: usize, source: Vec, target: Vec, bound: usize) -> Self { - assert!( - alphabet_size > 0 || (source.is_empty() && target.is_empty()), - "alphabet_size must be > 0 when source or target is non-empty" - ); - assert!( - source.iter().all(|&s| s < alphabet_size), - "all source symbols must be < alphabet_size" - ); - assert!( - target.iter().all(|&s| s < alphabet_size), - "all target symbols must be < alphabet_size" - ); - Self { + pub fn new( + alphabet_size: usize, + source: Vec, + target: Vec, + bound: usize, + ) -> Result { + if !(alphabet_size > 0 || (source.is_empty() && target.is_empty())) { + return Err("alphabet_size must be > 0 when source or target is non-empty".into()); + } + if !(source.iter().all(|&s| s < alphabet_size)) { + return Err("all source symbols must be < alphabet_size".into()); + } + if !(target.iter().all(|&s| s < alphabet_size)) { + return Err("all target symbols must be < alphabet_size".into()); + } + Ok(Self { alphabet_size, source, target, bound, - } + }) } /// Returns the alphabet size. @@ -246,8 +255,12 @@ impl Problem for StringToStringCorrection { } impl crate::solvers::BruteForceProblem for StringToStringCorrection { - fn dimensions(&self) -> Vec { - vec![2 * self.source.len() + 1; self.bound] + fn num_variables(&self) -> Result { + Ok(self.bound) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2 * self.source.len() + 1) } } @@ -266,12 +279,10 @@ pub(crate) fn canonical_model_example_specs() -> Vec, @@ -58,30 +59,49 @@ pub struct SubsetProduct { target: BigUint, } +#[derive(Deserialize)] +struct SubsetProductData { + #[serde(with = "super::biguint_serde::decimal_biguint_vec")] + sizes: Vec, + #[serde(with = "super::biguint_serde::decimal_biguint")] + target: BigUint, +} + +impl TryFrom for SubsetProduct { + type Error = crate::registry::ConstructionError; + fn try_from(data: SubsetProductData) -> Result { + if data.sizes.iter().any(BigUint::is_zero) { + return Err("all sizes must be positive (> 0)".into()); + } + if data.target.is_zero() { + return Err("SubsetProduct target must be positive".into()); + } + Ok(Self { + sizes: data.sizes, + target: data.target, + }) + } +} + impl SubsetProduct { /// Create a new SubsetProduct instance. /// - /// # Panics + /// # Errors /// - /// Panics if any size is not positive (must be > 0) or if target is zero. - pub fn new(sizes: Vec, target: T) -> Self + /// Returns an error if any size is not positive (must be > 0) or if target is zero. + pub fn new(sizes: Vec, target: T) -> Result where S: ToBigUint, T: ToBigUint, { - let sizes: Vec = sizes + let sizes = sizes .into_iter() - .map(|s| s.to_biguint().expect("All sizes must be positive (> 0)")) - .collect(); - assert!( - sizes.iter().all(|s| !s.is_zero()), - "All sizes must be positive (> 0)" - ); + .map(|size| size.to_biguint().ok_or("all sizes must be positive (> 0)")) + .collect::, _>>()?; let target = target .to_biguint() - .expect("SubsetProduct target must be nonnegative"); - assert!(!target.is_zero(), "SubsetProduct target must be positive"); - Self { sizes, target } + .ok_or("SubsetProduct target must be nonnegative")?; + SubsetProductData { sizes, target }.try_into() } /// Create a SubsetProduct without validating sizes (for testing edge cases). @@ -144,8 +164,12 @@ impl Problem for SubsetProduct { } impl crate::solvers::BruteForceProblem for SubsetProduct { - fn dimensions(&self) -> Vec { - vec![2; self.num_elements()] + fn num_variables(&self) -> Result { + Ok(self.num_elements()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -162,7 +186,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec, @@ -58,29 +59,46 @@ pub struct SubsetSum { target: BigUint, } +#[derive(Deserialize)] +struct SubsetSumData { + #[serde(with = "super::biguint_serde::decimal_biguint_vec")] + sizes: Vec, + #[serde(with = "super::biguint_serde::decimal_biguint")] + target: BigUint, +} + +impl TryFrom for SubsetSum { + type Error = crate::registry::ConstructionError; + fn try_from(data: SubsetSumData) -> Result { + if data.sizes.iter().any(BigUint::is_zero) { + return Err("all sizes must be positive (> 0)".into()); + } + Ok(Self { + sizes: data.sizes, + target: data.target, + }) + } +} + impl SubsetSum { /// Create a new SubsetSum instance. /// - /// # Panics + /// # Errors /// - /// Panics if any size is not positive (must be > 0). - pub fn new(sizes: Vec, target: T) -> Self + /// Returns an error if any size is not positive (must be > 0). + pub fn new(sizes: Vec, target: T) -> Result where S: ToBigUint, T: ToBigUint, { - let sizes: Vec = sizes + let sizes = sizes .into_iter() - .map(|s| s.to_biguint().expect("All sizes must be positive (> 0)")) - .collect(); - assert!( - sizes.iter().all(|s| !s.is_zero()), - "All sizes must be positive (> 0)" - ); + .map(|size| size.to_biguint().ok_or("all sizes must be positive (> 0)")) + .collect::, _>>()?; let target = target .to_biguint() - .expect("SubsetSum target must be nonnegative"); - Self { sizes, target } + .ok_or("SubsetSum target must be nonnegative")?; + SubsetSumData { sizes, target }.try_into() } /// Create a new SubsetSum instance without validating sizes. @@ -142,8 +160,12 @@ impl Problem for SubsetSum { } impl crate::solvers::BruteForceProblem for SubsetSum { - fn dimensions(&self) -> Vec { - vec![2; self.num_elements()] + fn num_variables(&self) -> Result { + Ok(self.num_elements()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -160,7 +182,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec { - vec![self.num_groups; self.sizes.len()] + fn num_variables(&self) -> Result { + Ok(self.sizes.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_groups) } } diff --git a/src/models/misc/three_partition.rs b/src/models/misc/three_partition.rs index 9a57d336c..81739756f 100644 --- a/src/models/misc/three_partition.rs +++ b/src/models/misc/three_partition.rs @@ -214,8 +214,12 @@ impl Problem for ThreePartition { } impl crate::solvers::BruteForceProblem for ThreePartition { - fn dimensions(&self) -> Vec { - vec![self.num_groups(); self.num_elements()] + fn num_variables(&self) -> Result { + Ok(self.num_elements()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_groups()) } } diff --git a/src/models/misc/timetable_design.rs b/src/models/misc/timetable_design.rs index b96bbe294..8c437744a 100644 --- a/src/models/misc/timetable_design.rs +++ b/src/models/misc/timetable_design.rs @@ -27,6 +27,7 @@ inventory::submit! { /// task-next, period-last order: /// `idx = ((c * num_tasks) + t) * num_periods + h`. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "TimetableDesignCreateSpec")] pub struct TimetableDesign { num_periods: usize, num_craftsmen: usize, @@ -54,86 +55,23 @@ struct TimetableDesignCreateSpec { impl TryFrom for TimetableDesign { type Error = crate::registry::ConstructionError; fn try_from(spec: TimetableDesignCreateSpec) -> Result { - if spec.craftsman_avail.len() != spec.num_craftsmen { - return Err(format!( - "craftsman_avail has {} rows, expected {}", - spec.craftsman_avail.len(), - spec.num_craftsmen - ) - .into()); - } - if let Some((index, row)) = spec - .craftsman_avail - .iter() - .enumerate() - .find(|(_, row)| row.len() != spec.num_periods) - { - return Err(format!( - "craftsman_avail row {index} has {} periods, expected {}", - row.len(), - spec.num_periods - ) - .into()); - } - if spec.task_avail.len() != spec.num_tasks { - return Err(format!( - "task_avail has {} rows, expected {}", - spec.task_avail.len(), - spec.num_tasks - ) - .into()); - } - if let Some((index, row)) = spec - .task_avail - .iter() - .enumerate() - .find(|(_, row)| row.len() != spec.num_periods) - { - return Err(format!( - "task_avail row {index} has {} periods, expected {}", - row.len(), - spec.num_periods - ) - .into()); - } - if spec.requirements.len() != spec.num_craftsmen { - return Err(format!( - "requirements has {} rows, expected {}", - spec.requirements.len(), - spec.num_craftsmen - ) - .into()); - } - if let Some((index, row)) = spec - .requirements - .iter() - .enumerate() - .find(|(_, row)| row.len() != spec.num_tasks) - { - return Err(format!( - "requirements row {index} has {} tasks, expected {}", - row.len(), - spec.num_tasks - ) - .into()); - } - Ok(Self::new( + Self::new( spec.num_periods, spec.num_craftsmen, spec.num_tasks, spec.craftsman_avail, spec.task_avail, spec.requirements, - )) + ) } } impl TimetableDesign { /// Create a new Timetable Design instance. /// - /// # Panics + /// # Errors /// - /// Panics if any matrix dimensions do not match the declared counts. + /// Returns an error if any matrix dimensions do not match the declared counts. pub fn new( num_periods: usize, num_craftsmen: usize, @@ -141,69 +79,75 @@ impl TimetableDesign { craftsman_avail: Vec>, task_avail: Vec>, requirements: Vec>, - ) -> Self { - assert_eq!( - craftsman_avail.len(), - num_craftsmen, - "craftsman_avail has {} rows, expected {}", - craftsman_avail.len(), - num_craftsmen - ); + ) -> Result { + if craftsman_avail.len() != num_craftsmen { + return Err(format!( + "craftsman_avail has {} rows, expected {}", + craftsman_avail.len(), + num_craftsmen + ) + .into()); + } for (craftsman, row) in craftsman_avail.iter().enumerate() { - assert_eq!( - row.len(), - num_periods, - "craftsman {} availability has {} periods, expected {}", - craftsman, - row.len(), - num_periods - ); + if row.len() != num_periods { + return Err(format!( + "craftsman {} availability has {} periods, expected {}", + craftsman, + row.len(), + num_periods + ) + .into()); + } } - assert_eq!( - task_avail.len(), - num_tasks, - "task_avail has {} rows, expected {}", - task_avail.len(), - num_tasks - ); + if task_avail.len() != num_tasks { + return Err(format!( + "task_avail has {} rows, expected {}", + task_avail.len(), + num_tasks + ) + .into()); + } for (task, row) in task_avail.iter().enumerate() { - assert_eq!( - row.len(), - num_periods, - "task {} availability has {} periods, expected {}", - task, - row.len(), - num_periods - ); + if row.len() != num_periods { + return Err(format!( + "task {} availability has {} periods, expected {}", + task, + row.len(), + num_periods + ) + .into()); + } } - assert_eq!( - requirements.len(), - num_craftsmen, - "requirements has {} rows, expected {}", - requirements.len(), - num_craftsmen - ); + if requirements.len() != num_craftsmen { + return Err(format!( + "requirements has {} rows, expected {}", + requirements.len(), + num_craftsmen + ) + .into()); + } for (craftsman, row) in requirements.iter().enumerate() { - assert_eq!( - row.len(), - num_tasks, - "requirements row {} has {} tasks, expected {}", - craftsman, - row.len(), - num_tasks - ); + if row.len() != num_tasks { + return Err(format!( + "requirements row {} has {} tasks, expected {}", + craftsman, + row.len(), + num_tasks + ) + .into()); + } } - Self { + Ok(Self { num_periods, num_craftsmen, num_tasks, craftsman_avail, task_avail, requirements, - } + }) } /// Get the number of periods. @@ -475,8 +419,12 @@ impl Problem for TimetableDesign { } impl crate::solvers::BruteForceProblem for TimetableDesign { - fn dimensions(&self) -> Vec { - vec![2; self.config_len()] + fn num_variables(&self) -> Result { + Ok(self.config_len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -527,6 +475,7 @@ fn issue_example_problem() -> TimetableDesign { vec![0, 1, 0, 0, 0], ], ) + .unwrap() } #[cfg(any(test, feature = "example-db"))] diff --git a/src/models/mod.rs b/src/models/mod.rs index 21220aa30..b01593468 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -43,9 +43,8 @@ pub use graph::{ PartialFeedbackEdgeSet, PartitionIntoCliques, PartitionIntoForests, PartitionIntoPathsOfLength2, PartitionIntoPerfectMatchings, PartitionIntoTriangles, PathConstrainedNetworkFlow, RootedTreeArrangement, RuralPostman, ShortestWeightConstrainedPath, - SpinGlass, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation, - SubgraphIsomorphism, TravelingSalesman, UndirectedFlowLowerBounds, - UndirectedTwoCommodityIntegralFlow, + SpinGlass, SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, + UndirectedFlowLowerBounds, UndirectedTwoCommodityIntegralFlow, }; pub use misc::PartiallyOrderedKnapsack; pub use misc::{ diff --git a/src/models/set/comparative_containment.rs b/src/models/set/comparative_containment.rs index f628c9744..398136d5c 100644 --- a/src/models/set/comparative_containment.rs +++ b/src/models/set/comparative_containment.rs @@ -340,8 +340,12 @@ impl crate::solvers::BruteForceProblem for ComparativeContainment where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.universe_size] + fn num_variables(&self) -> Result { + Ok(self.universe_size) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/set/consecutive_sets.rs b/src/models/set/consecutive_sets.rs index 9fc0aa171..a3f76d6c4 100644 --- a/src/models/set/consecutive_sets.rs +++ b/src/models/set/consecutive_sets.rs @@ -52,7 +52,7 @@ inventory::submit! { /// 6, /// vec![vec![0, 4], vec![2, 4], vec![2, 5], vec![1, 5], vec![1, 3]], /// 6, -/// ); +/// ).unwrap(); /// /// let solver = BruteForce::new(); /// let solution = solver.solve(&problem).unwrap(); @@ -62,7 +62,7 @@ inventory::submit! { /// assert!(problem.evaluate(&solution.unwrap()).unwrap()); /// /// // Shorter strings use trailing `None` positions. -/// let shorter = ConsecutiveSets::new(3, vec![vec![0, 1]], 4); +/// let shorter = ConsecutiveSets::new(3, vec![vec![0, 1]], 4).unwrap(); /// assert!(shorter /// .evaluate(&vec![Some(0), Some(1), None, None]) /// .unwrap()); @@ -71,6 +71,7 @@ inventory::submit! { /// .unwrap()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ConsecutiveSetsData")] pub struct ConsecutiveSets { /// Size of the alphabet (elements are 0..alphabet_size-1). alphabet_size: usize, @@ -80,39 +81,52 @@ pub struct ConsecutiveSets { bound_k: usize, } +#[derive(Deserialize)] +struct ConsecutiveSetsData { + alphabet_size: usize, + subsets: Vec>, + bound_k: usize, +} + +impl TryFrom for ConsecutiveSets { + type Error = crate::registry::ConstructionError; + + fn try_from(data: ConsecutiveSetsData) -> Result { + Self::new(data.alphabet_size, data.subsets, data.bound_k) + } +} + impl ConsecutiveSets { /// Create a new Consecutive Sets problem. /// - /// # Panics + /// # Errors /// - /// Panics if `bound_k` is zero, if any subset contains duplicate elements, - /// or if any element is outside the alphabet. - pub fn new(alphabet_size: usize, subsets: Vec>, bound_k: usize) -> Self { - assert!(bound_k > 0, "bound_k must be positive, got 0"); - let mut subsets = subsets; - for (i, subset) in subsets.iter_mut().enumerate() { + /// Returns an error when the instance violates its documented input conditions. + pub fn new( + alphabet_size: usize, + mut subsets: Vec>, + bound_k: usize, + ) -> Result { + if bound_k == 0 { + return Err("bound_k must be positive, got 0".into()); + } + for (index, subset) in subsets.iter_mut().enumerate() { let mut seen = HashSet::with_capacity(subset.len()); - for &elem in subset.iter() { - assert!( - elem < alphabet_size, - "Subset {} contains element {} which is outside alphabet of size {}", - i, - elem, - alphabet_size - ); - assert!( - seen.insert(elem), - "Subset {} contains duplicate elements", - i - ); + for &element in subset.iter() { + if element >= alphabet_size { + return Err(format!("subset {index} contains element {element} outside alphabet of size {alphabet_size}").into()); + } + if !seen.insert(element) { + return Err(format!("subset {index} contains duplicate elements").into()); + } } subset.sort(); } - Self { + Ok(Self { alphabet_size, subsets, bound_k, - } + }) } /// Get the alphabet size. @@ -244,9 +258,14 @@ impl Problem for ConsecutiveSets { } impl crate::solvers::BruteForceProblem for ConsecutiveSets { - fn dimensions(&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 num_variables(&self) -> Result { + Ok(self.bound_k) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.alphabet_size).checked_add(1usize).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } @@ -263,11 +282,14 @@ pub(crate) fn canonical_model_example_specs() -> Vec for ExactCoverBy3Sets { type Error = crate::registry::ConstructionError; - fn try_from(mut spec: ExactCoverBy3SetsCreateSpec) -> Result { - if !spec.universe_size.is_multiple_of(3) { + fn try_from(spec: ExactCoverBy3SetsCreateSpec) -> Result { + Self::new(spec.universe_size, spec.subsets) + } +} + +impl ExactCoverBy3Sets { + /// Create a new X3C problem. + /// + /// # Errors + /// + /// Returns an error if the universe size is not divisible by three, or a + /// subset contains repeated or out-of-range elements. + pub fn new( + universe_size: usize, + mut subsets: Vec<[usize; 3]>, + ) -> Result { + if !universe_size.is_multiple_of(3) { return Err("universe_size must be divisible by 3".into()); } - for (index, subset) in spec.subsets.iter_mut().enumerate() { + for (index, subset) in subsets.iter_mut().enumerate() { if subset[0] == subset[1] || subset[0] == subset[2] || subset[1] == subset[2] { return Err(format!("subset {index} contains duplicate elements").into()); } - if let Some(&element) = subset - .iter() - .find(|&&element| element >= spec.universe_size) - { + if let Some(&element) = subset.iter().find(|&&element| element >= universe_size) { return Err( format!("subset {index} contains out-of-range element {element}").into(), ); @@ -87,48 +100,9 @@ impl TryFrom for ExactCoverBy3Sets { subset.sort(); } Ok(Self { - universe_size: spec.universe_size, - subsets: spec.subsets, - }) - } -} - -impl ExactCoverBy3Sets { - /// Create a new X3C problem. - /// - /// # Panics - /// - /// Panics if `universe_size` is not divisible by 3, or if any subset - /// contains duplicate elements or elements outside the universe. - pub fn new(universe_size: usize, subsets: Vec<[usize; 3]>) -> Self { - assert!( - universe_size.is_multiple_of(3), - "Universe size must be divisible by 3, got {}", - universe_size - ); - let mut subsets = subsets; - for (i, subset) in subsets.iter_mut().enumerate() { - assert!( - subset[0] != subset[1] && subset[0] != subset[2] && subset[1] != subset[2], - "Subset {} contains duplicate elements: {:?}", - i, - subset - ); - for &elem in subset.iter() { - assert!( - elem < universe_size, - "Subset {} contains element {} which is outside universe of size {}", - i, - elem, - universe_size - ); - } - subset.sort(); - } - Self { universe_size, subsets, - } + }) } /// Get the universe size. @@ -239,8 +213,12 @@ impl Problem for ExactCoverBy3Sets { } impl crate::solvers::BruteForceProblem for ExactCoverBy3Sets { - fn dimensions(&self) -> Vec { - vec![2; self.subsets.len()] + fn num_variables(&self) -> Result { + Ok(self.subsets.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -256,18 +234,21 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "exact_cover_by_3_sets", - instance: Box::new(ExactCoverBy3Sets::new( - 9, - vec![ - [0, 1, 2], - [0, 2, 4], - [3, 4, 5], - [3, 5, 7], - [6, 7, 8], - [1, 4, 6], - [2, 5, 8], - ], - )), + instance: Box::new( + ExactCoverBy3Sets::new( + 9, + vec![ + [0, 1, 2], + [0, 2, 4], + [3, 4, 5], + [3, 5, 7], + [6, 7, 8], + [1, 4, 6], + [2, 5, 8], + ], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![true, false, true, false, true, false, false]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/set/integer_knapsack.rs b/src/models/set/integer_knapsack.rs index 6080cbdff..a0a38ad05 100644 --- a/src/models/set/integer_knapsack.rs +++ b/src/models/set/integer_knapsack.rs @@ -5,7 +5,6 @@ use crate::registry::ConstructionError; use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Max; use serde::{Deserialize, Serialize}; @@ -115,16 +114,6 @@ impl Problem for IntegerKnapsack { "multiplicity-vector length does not match the items".into(), )); } - let dims = self.dimensions(); - if config - .iter() - .zip(&dims) - .any(|(&count, &dimension)| count >= dimension) - { - return Err(crate::traits::EvaluationError::InvalidConfiguration( - "multiplicity vector contains an out-of-range count".into(), - )); - } let total_size = config .iter() .enumerate() @@ -174,15 +163,14 @@ impl Problem for IntegerKnapsack { } impl crate::solvers::BruteForceProblem for IntegerKnapsack { - fn dimensions(&self) -> Vec { - self.sizes - .iter() - .map(|&s| { - let dimension = i128::from(self.capacity) / i128::from(s) + 1; - usize::try_from(dimension) - .expect("validated integer-knapsack dimension must fit usize") - }) - .collect() + fn num_variables(&self) -> Result { + Ok(self.num_items()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from( + i128::from(self.capacity / self.sizes[variable]) + 1, + )?) } } @@ -239,15 +227,6 @@ impl TryFrom for IntegerKnapsack { raw.capacity ))); } - for &size in &raw.sizes { - let dimension = i128::from(raw.capacity) / i128::from(size) + 1; - usize::try_from(dimension).map_err(|_| { - ConstructionError::IntegerOverflow(format!( - "knapsack dimension for capacity {} and item size {size} does not fit usize", - raw.capacity - )) - })?; - } Ok(IntegerKnapsack { sizes: raw.sizes, values: raw.values, diff --git a/src/models/set/maximum_set_packing.rs b/src/models/set/maximum_set_packing.rs index 40542427c..798220bf5 100644 --- a/src/models/set/maximum_set_packing.rs +++ b/src/models/set/maximum_set_packing.rs @@ -225,8 +225,12 @@ impl crate::solvers::BruteForceProblem for MaximumSetPacking where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.sets.len()] + fn num_variables(&self) -> Result { + Ok(self.sets.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/set/minimum_cardinality_key.rs b/src/models/set/minimum_cardinality_key.rs index 89b3a21ac..0a1b942ce 100644 --- a/src/models/set/minimum_cardinality_key.rs +++ b/src/models/set/minimum_cardinality_key.rs @@ -31,6 +31,7 @@ inventory::submit! { /// find a subset `K ⊆ A` of minimum cardinality such that the closure of `K` /// under `F` equals `A` (i.e., `K` is a key). #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumCardinalityKeyData")] pub struct MinimumCardinalityKey { /// Number of attributes (elements are `0..num_attributes`). num_attributes: usize, @@ -38,34 +39,47 @@ pub struct MinimumCardinalityKey { dependencies: Vec<(Vec, Vec)>, } +#[derive(Deserialize)] +struct MinimumCardinalityKeyData { + num_attributes: usize, + dependencies: Vec<(Vec, Vec)>, +} + +impl TryFrom for MinimumCardinalityKey { + type Error = crate::registry::ConstructionError; + + fn try_from(data: MinimumCardinalityKeyData) -> Result { + Self::new(data.num_attributes, data.dependencies) + } +} + impl MinimumCardinalityKey { /// Create a new Minimum Cardinality Key instance. /// - /// # Panics + /// # Errors /// - /// Panics if any attribute index in a dependency lies outside the attribute set. - pub fn new(num_attributes: usize, dependencies: Vec<(Vec, Vec)>) -> Self { - let mut dependencies = dependencies; - for (dep_index, (lhs, rhs)) in dependencies.iter_mut().enumerate() { + /// Returns an error when the instance violates its documented input conditions. + pub fn new( + num_attributes: usize, + mut dependencies: Vec<(Vec, Vec)>, + ) -> Result { + for (index, (lhs, rhs)) in dependencies.iter_mut().enumerate() { lhs.sort_unstable(); lhs.dedup(); rhs.sort_unstable(); rhs.dedup(); - for &attr in lhs.iter().chain(rhs.iter()) { - assert!( - attr < num_attributes, - "Dependency {} contains attribute {} which is outside attribute set of size {}", - dep_index, - attr, - num_attributes - ); + if let Some(attribute) = lhs + .iter() + .chain(rhs.iter()) + .find(|&&attribute| attribute >= num_attributes) + { + return Err(format!("dependency {index} contains attribute {attribute} outside attribute set of size {num_attributes}").into()); } } - - Self { + Ok(Self { num_attributes, dependencies, - } + }) } /// Return the number of attributes. @@ -156,8 +170,12 @@ impl Problem for MinimumCardinalityKey { } impl crate::solvers::BruteForceProblem for MinimumCardinalityKey { - fn dimensions(&self) -> Vec { - vec![2; self.num_attributes] + fn num_variables(&self) -> Result { + Ok(self.num_attributes) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -173,15 +191,18 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "minimum_cardinality_key", - instance: Box::new(MinimumCardinalityKey::new( - 6, - vec![ - (vec![0, 1], vec![2]), - (vec![0, 2], vec![3]), - (vec![1, 3], vec![4]), - (vec![2, 4], vec![5]), - ], - )), + instance: Box::new( + MinimumCardinalityKey::new( + 6, + vec![ + (vec![0, 1], vec![2]), + (vec![0, 2], vec![3]), + (vec![1, 3], vec![4]), + (vec![2, 4], vec![5]), + ], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![true, true, false, false, false, false]), optimal_value: serde_json::json!(2), }] diff --git a/src/models/set/minimum_hitting_set.rs b/src/models/set/minimum_hitting_set.rs index ca2aa38d2..fca3278be 100644 --- a/src/models/set/minimum_hitting_set.rs +++ b/src/models/set/minimum_hitting_set.rs @@ -26,11 +26,26 @@ inventory::submit! { /// Given a universe `U` and a collection of subsets of `U`, find a minimum-size /// subset `H ⊆ U` such that `H` intersects every set in the collection. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumHittingSetData")] pub struct MinimumHittingSet { universe_size: usize, sets: Vec>, } +#[derive(Deserialize)] +struct MinimumHittingSetData { + universe_size: usize, + sets: Vec>, +} + +impl TryFrom for MinimumHittingSet { + type Error = crate::registry::ConstructionError; + + fn try_from(data: MinimumHittingSetData) -> Result { + Self::new(data.universe_size, data.sets) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumHittingSetCreateSpec { /// Size of the universe U. @@ -43,42 +58,31 @@ impl TryFrom for MinimumHittingSet { type Error = crate::registry::ConstructionError; fn try_from(spec: MinimumHittingSetCreateSpec) -> Result { - for (set_index, set) in spec.subsets.iter().enumerate() { - if let Some(&element) = set.iter().find(|&&element| element >= spec.universe_size) { - return Err(format!( - "subsets[{set_index}] contains element {element} outside universe of size {}", - spec.universe_size - ) - .into()); - } - } - Ok(Self::new(spec.universe_size, spec.subsets)) + Self::new(spec.universe_size, spec.subsets) } } impl MinimumHittingSet { /// Create a new Minimum Hitting Set instance. /// - /// # Panics + /// # Errors /// - /// Panics if any set contains an element outside `0..universe_size`. - pub fn new(universe_size: usize, sets: Vec>) -> Self { - let mut sets = sets; - for (set_index, set) in sets.iter_mut().enumerate() { + /// Returns an error when the instance violates its documented input conditions. + pub fn new( + universe_size: usize, + mut sets: Vec>, + ) -> Result { + for (index, set) in sets.iter_mut().enumerate() { set.sort_unstable(); set.dedup(); - for &element in set.iter() { - assert!( - element < universe_size, - "Set {set_index} contains element {element} which is outside universe of size {universe_size}" - ); + if let Some(element) = set.iter().find(|&&element| element >= universe_size) { + return Err(format!("set {index} contains element {element} outside universe of size {universe_size}").into()); } } - - Self { + Ok(Self { universe_size, sets, - } + }) } /// Get the universe size. @@ -168,8 +172,12 @@ impl Problem for MinimumHittingSet { } impl crate::solvers::BruteForceProblem for MinimumHittingSet { - fn dimensions(&self) -> Vec { - vec![2; self.universe_size] + fn num_variables(&self) -> Result { + Ok(self.universe_size) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -185,18 +193,21 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "minimum_hitting_set", - instance: Box::new(MinimumHittingSet::new( - 6, - vec![ - vec![0, 1, 2], - vec![0, 3, 4], - vec![1, 3, 5], - vec![2, 4, 5], - vec![0, 1, 5], - vec![2, 3], - vec![1, 4], - ], - )), + instance: Box::new( + MinimumHittingSet::new( + 6, + vec![ + vec![0, 1, 2], + vec![0, 3, 4], + vec![1, 3, 5], + vec![2, 4, 5], + vec![0, 1, 5], + vec![2, 3], + vec![1, 4], + ], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![false, true, false, true, true, false]), optimal_value: serde_json::json!(3), }] diff --git a/src/models/set/minimum_set_covering.rs b/src/models/set/minimum_set_covering.rs index 81748fc00..2229362ad 100644 --- a/src/models/set/minimum_set_covering.rs +++ b/src/models/set/minimum_set_covering.rs @@ -45,7 +45,7 @@ inventory::submit! { /// vec![2, 3], /// vec![0, 3], /// ], -/// ); +/// ).unwrap(); /// /// let solver = BruteForce::new(); /// let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -55,7 +55,7 @@ inventory::submit! { /// assert!(problem.evaluate(&sol).unwrap().is_valid()); /// } /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumSetCovering { /// Size of the universe (elements are 0..universe_size). universe_size: usize, @@ -65,6 +65,21 @@ pub struct MinimumSetCovering { weights: Vec, } +#[derive(Deserialize)] +struct MinimumSetCoveringData { + universe_size: usize, + sets: Vec>, + weights: Vec, +} + +impl<'de, W: Clone + Default + Deserialize<'de>> Deserialize<'de> for MinimumSetCovering { + fn deserialize>(deserializer: D) -> Result { + let data = MinimumSetCoveringData::deserialize(deserializer)?; + Self::with_weights(data.universe_size, data.sets, data.weights) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumSetCoveringCreateSpec { /// Size of the universe U. @@ -79,54 +94,49 @@ impl TryFrom for MinimumSetCovering { type Error = crate::registry::ConstructionError; fn try_from(spec: MinimumSetCoveringCreateSpec) -> Result { - if spec.subsets.len() != spec.weights.len() { - return Err(format!( - "weights has {} entries, expected one for each of {} subsets", - spec.weights.len(), - spec.subsets.len() - ) - .into()); - } - for (set_index, set) in spec.subsets.iter().enumerate() { - if let Some(&element) = set.iter().find(|&&element| element >= spec.universe_size) { - return Err(format!( - "subsets[{set_index}] contains element {element} outside universe of size {}", - spec.universe_size - ) - .into()); - } - } - Ok(Self::with_weights( - spec.universe_size, - spec.subsets, - spec.weights, - )) + Self::with_weights(spec.universe_size, spec.subsets, spec.weights) } } impl MinimumSetCovering { /// Create a new Set Covering problem with unit weights. - pub fn new(universe_size: usize, sets: Vec>) -> Self + pub fn new( + universe_size: usize, + sets: Vec>, + ) -> Result where W: WeightElement, { - let num_sets = sets.len(); - let weights = vec![W::unit(); num_sets]; - Self { - universe_size, - sets, - weights, - } + let weights = vec![W::unit(); sets.len()]; + Self::with_weights(universe_size, sets, weights) } /// Create a new Set Covering problem with custom weights. - pub fn with_weights(universe_size: usize, sets: Vec>, weights: Vec) -> Self { - assert_eq!(sets.len(), weights.len()); - Self { + /// + /// Returns an error if weights do not match the sets or an element is out of range. + pub fn with_weights( + universe_size: usize, + sets: Vec>, + weights: Vec, + ) -> Result { + if sets.len() != weights.len() { + return Err(format!( + "weights has {} entries, expected one for each of {} subsets", + weights.len(), + sets.len() + ) + .into()); + } + for (index, set) in sets.iter().enumerate() { + if let Some(element) = set.iter().find(|&&element| element >= universe_size) { + return Err(format!("set {index} contains element {element} outside universe of size {universe_size}").into()); + } + } + Ok(Self { universe_size, sets, weights, - } + }) } /// Get the universe size. @@ -223,8 +233,12 @@ impl crate::solvers::BruteForceProblem for MinimumSetCovering where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.sets.len()] + fn num_variables(&self) -> Result { + Ok(self.sets.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -257,10 +271,10 @@ pub(crate) fn is_set_cover(universe_size: usize, sets: &[Vec], selected: pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "minimum_set_covering", - instance: Box::new(MinimumSetCovering::::new( - 5, - vec![vec![0, 1, 2], vec![1, 3], vec![2, 3, 4]], - )), + instance: Box::new( + MinimumSetCovering::::new(5, vec![vec![0, 1, 2], vec![1, 3], vec![2, 3, 4]]) + .unwrap(), + ), optimal_config: serde_json::json!(vec![true, false, true]), optimal_value: serde_json::json!(2), }] diff --git a/src/models/set/prime_attribute_name.rs b/src/models/set/prime_attribute_name.rs index 42a3c1839..cd6c4c963 100644 --- a/src/models/set/prime_attribute_name.rs +++ b/src/models/set/prime_attribute_name.rs @@ -48,7 +48,7 @@ inventory::submit! { /// (vec![0, 3], vec![1, 2, 4, 5]), /// ], /// 3, -/// ); +/// ).unwrap(); /// /// // {2, 3} is a candidate key containing attribute 3 /// assert!(problem @@ -60,6 +60,7 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "PrimeAttributeNameData")] pub struct PrimeAttributeName { /// Number of attributes (elements are 0..num_attributes). num_attributes: usize, @@ -69,6 +70,21 @@ pub struct PrimeAttributeName { query_attribute: usize, } +#[derive(Deserialize)] +struct PrimeAttributeNameData { + num_attributes: usize, + dependencies: Vec<(Vec, Vec)>, + query_attribute: usize, +} + +impl TryFrom for PrimeAttributeName { + type Error = crate::registry::ConstructionError; + + fn try_from(data: PrimeAttributeNameData) -> Result { + Self::new(data.num_attributes, data.dependencies, data.query_attribute) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct PrimeAttributeNameCreateSpec { /// Number of attributes. @@ -83,73 +99,41 @@ impl TryFrom for PrimeAttributeName { type Error = crate::registry::ConstructionError; fn try_from(spec: PrimeAttributeNameCreateSpec) -> Result { - if spec.query_attribute >= spec.universe_size { - return Err(format!( - "query_attribute {} is outside universe of size {}", - spec.query_attribute, spec.universe_size - ) - .into()); - } - for (dependency_index, (lhs, rhs)) in spec.dependencies.iter().enumerate() { - if lhs.is_empty() { - return Err( - format!("dependencies[{dependency_index}] has an empty left side").into(), - ); - } - if let Some(&attribute) = lhs - .iter() - .chain(rhs) - .find(|&&attribute| attribute >= spec.universe_size) - { - return Err(format!( - "dependencies[{dependency_index}] contains attribute {attribute} outside universe of size {}", - spec.universe_size - ).into()); - } - } - Ok(Self::new( - spec.universe_size, - spec.dependencies, - spec.query_attribute, - )) + Self::new(spec.universe_size, spec.dependencies, spec.query_attribute) } } impl PrimeAttributeName { /// Create a new Prime Attribute Name problem. /// - /// # Panics + /// # Errors /// - /// Panics if `query_attribute >= num_attributes`, if any attribute index - /// in a dependency is out of range, or if any LHS is empty. + /// Returns an error when the instance violates its documented input conditions. pub fn new( num_attributes: usize, dependencies: Vec<(Vec, Vec)>, query_attribute: usize, - ) -> Self { - assert!( - query_attribute < num_attributes, - "Query attribute {} is outside attribute set of size {}", - query_attribute, - num_attributes - ); - for (i, (lhs, rhs)) in dependencies.iter().enumerate() { - assert!(!lhs.is_empty(), "Dependency {} has empty LHS", i); - for &attr in lhs.iter().chain(rhs.iter()) { - assert!( - attr < num_attributes, - "Dependency {} references attribute {} which is outside attribute set of size {}", - i, - attr, - num_attributes - ); + ) -> Result { + if query_attribute >= num_attributes { + return Err(format!("query attribute {query_attribute} is outside attribute set of size {num_attributes}").into()); + } + for (index, (lhs, rhs)) in dependencies.iter().enumerate() { + if lhs.is_empty() { + return Err(format!("dependency {index} has an empty left side").into()); + } + if let Some(attribute) = lhs + .iter() + .chain(rhs.iter()) + .find(|&&attribute| attribute >= num_attributes) + { + return Err(format!("dependency {index} contains attribute {attribute} outside attribute set of size {num_attributes}").into()); } } - Self { + Ok(Self { num_attributes, dependencies, query_attribute, - } + }) } /// Get the number of attributes. @@ -255,8 +239,12 @@ impl Problem for PrimeAttributeName { } impl crate::solvers::BruteForceProblem for PrimeAttributeName { - fn dimensions(&self) -> Vec { - vec![2; self.num_attributes] + fn num_variables(&self) -> Result { + Ok(self.num_attributes) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -273,15 +261,18 @@ pub(crate) fn canonical_model_example_specs() -> Vec YES - instance: Box::new(PrimeAttributeName::new( - 6, - vec![ - (vec![0, 1], vec![2, 3, 4, 5]), - (vec![2, 3], vec![0, 1, 4, 5]), - (vec![0, 3], vec![1, 2, 4, 5]), - ], - 3, - )), + instance: Box::new( + PrimeAttributeName::new( + 6, + vec![ + (vec![0, 1], vec![2, 3, 4, 5]), + (vec![2, 3], vec![0, 1, 4, 5]), + (vec![0, 3], vec![1, 2, 4, 5]), + ], + 3, + ) + .unwrap(), + ), // {2, 3} is a candidate key containing attribute 3 optimal_config: serde_json::json!(vec![false, false, true, true, false, false]), optimal_value: serde_json::json!(true), diff --git a/src/models/set/rooted_tree_storage_assignment.rs b/src/models/set/rooted_tree_storage_assignment.rs index 1593976d0..c609f70af 100644 --- a/src/models/set/rooted_tree_storage_assignment.rs +++ b/src/models/set/rooted_tree_storage_assignment.rs @@ -238,8 +238,12 @@ impl Problem for RootedTreeStorageAssignment { } impl crate::solvers::BruteForceProblem for RootedTreeStorageAssignment { - fn dimensions(&self) -> Vec { - vec![self.universe_size; self.universe_size] + fn num_variables(&self) -> Result { + Ok(self.universe_size) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.universe_size) } } diff --git a/src/models/set/set_basis.rs b/src/models/set/set_basis.rs index be76ee298..e38e3c0f4 100644 --- a/src/models/set/set_basis.rs +++ b/src/models/set/set_basis.rs @@ -28,6 +28,7 @@ inventory::submit! { /// `S` such that every set in `C` can be expressed as the union of some /// subcollection of `B`. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "SetBasisData")] pub struct SetBasis { /// Size of the universe (elements are `0..universe_size`). universe_size: usize, @@ -37,6 +38,21 @@ pub struct SetBasis { k: usize, } +#[derive(Deserialize)] +struct SetBasisData { + universe_size: usize, + collection: Vec>, + k: usize, +} + +impl TryFrom for SetBasis { + type Error = crate::registry::ConstructionError; + + fn try_from(data: SetBasisData) -> Result { + Self::new(data.universe_size, data.collection, data.k) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct SetBasisCreateSpec { /// Size of the ground set S. @@ -51,46 +67,33 @@ impl TryFrom for SetBasis { type Error = crate::registry::ConstructionError; fn try_from(spec: SetBasisCreateSpec) -> Result { - for (set_index, set) in spec.subsets.iter().enumerate() { - if let Some(&element) = set.iter().find(|&&element| element >= spec.universe_size) { - return Err(format!( - "subsets[{set_index}] contains element {element} outside universe of size {}", - spec.universe_size - ) - .into()); - } - } - Ok(Self::new(spec.universe_size, spec.subsets, spec.k)) + Self::new(spec.universe_size, spec.subsets, spec.k) } } impl SetBasis { /// Create a new Set Basis instance. /// - /// # Panics + /// # Errors /// - /// Panics if any element in `collection` lies outside the universe. - pub fn new(universe_size: usize, collection: Vec>, k: usize) -> Self { - let mut collection = collection; - for (set_index, set) in collection.iter_mut().enumerate() { + /// Returns an error when the instance violates its documented input conditions. + pub fn new( + universe_size: usize, + mut collection: Vec>, + k: usize, + ) -> Result { + for (index, set) in collection.iter_mut().enumerate() { set.sort_unstable(); set.dedup(); - for &element in set.iter() { - assert!( - element < universe_size, - "Set {} contains element {} which is outside universe of size {}", - set_index, - element, - universe_size - ); + if let Some(element) = set.iter().find(|&&element| element >= universe_size) { + return Err(format!("set {index} contains element {element} outside universe of size {universe_size}").into()); } } - - Self { + Ok(Self { universe_size, collection, k, - } + }) } /// Return the universe size. @@ -158,9 +161,6 @@ impl SetBasis { fn can_represent_target(basis: &[Vec], target: &[usize], universe_size: usize) -> bool { let mut target_membership = vec![false; universe_size]; for &element in target { - if element >= universe_size { - return false; - } target_membership[element] = true; } @@ -201,8 +201,14 @@ impl Problem for SetBasis { } impl crate::solvers::BruteForceProblem for SetBasis { - fn dimensions(&self) -> Vec { - vec![2; self.k * self.universe_size] + fn num_variables(&self) -> Result { + (self.k).checked_mul(self.universe_size).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -218,11 +224,14 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "set_basis", - instance: Box::new(SetBasis::new( - 4, - vec![vec![0, 1], vec![1, 2], vec![0, 2], vec![0, 1, 2]], - 3, - )), + instance: Box::new( + SetBasis::new( + 4, + vec![vec![0, 1], vec![1, 2], vec![0, 2], vec![0, 1, 2]], + 3, + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![ vec![false, false, true, false], vec![false, true, false, false], diff --git a/src/models/set/set_splitting.rs b/src/models/set/set_splitting.rs index 1c7f46a76..adf234449 100644 --- a/src/models/set/set_splitting.rs +++ b/src/models/set/set_splitting.rs @@ -210,8 +210,12 @@ impl Problem for SetSplitting { } impl crate::solvers::BruteForceProblem for SetSplitting { - fn dimensions(&self) -> Vec { - vec![2; self.universe_size] + fn num_variables(&self) -> Result { + Ok(self.universe_size) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/set/three_dimensional_matching.rs b/src/models/set/three_dimensional_matching.rs index 6b7fb2641..293db7790 100644 --- a/src/models/set/three_dimensional_matching.rs +++ b/src/models/set/three_dimensional_matching.rs @@ -46,7 +46,7 @@ inventory::submit! { /// let problem = ThreeDimensionalMatching::new( /// 3, /// vec![(0, 1, 2), (1, 0, 1), (2, 2, 0), (0, 0, 0), (1, 2, 2)], -/// ); +/// ).unwrap(); /// /// let solver = BruteForce::new(); /// let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -56,6 +56,7 @@ inventory::submit! { /// assert!(problem.evaluate(&solutions[0]).unwrap()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ThreeDimensionalMatchingData")] pub struct ThreeDimensionalMatching { /// Size of each set W, X, Y (elements are 0..universe_size). universe_size: usize, @@ -63,40 +64,42 @@ pub struct ThreeDimensionalMatching { triples: Vec<(usize, usize, usize)>, } +#[derive(Deserialize)] +struct ThreeDimensionalMatchingData { + universe_size: usize, + triples: Vec<(usize, usize, usize)>, +} + +impl TryFrom for ThreeDimensionalMatching { + type Error = crate::registry::ConstructionError; + + fn try_from(data: ThreeDimensionalMatchingData) -> Result { + Self::new(data.universe_size, data.triples) + } +} + impl ThreeDimensionalMatching { /// Create a new 3DM problem. /// - /// # Panics + /// # Errors /// - /// Panics if any triple contains an element outside 0..universe_size. - pub fn new(universe_size: usize, triples: Vec<(usize, usize, usize)>) -> Self { - for (i, &(w, x, y)) in triples.iter().enumerate() { - assert!( - w < universe_size, - "Triple {} has w-coordinate {} which is outside 0..{}", - i, - w, - universe_size - ); - assert!( - x < universe_size, - "Triple {} has x-coordinate {} which is outside 0..{}", - i, - x, - universe_size - ); - assert!( - y < universe_size, - "Triple {} has y-coordinate {} which is outside 0..{}", - i, - y, - universe_size - ); + /// Returns an error when the instance violates its documented input conditions. + pub fn new( + universe_size: usize, + triples: Vec<(usize, usize, usize)>, + ) -> Result { + for (index, &(w, x, y)) in triples.iter().enumerate() { + if w >= universe_size || x >= universe_size || y >= universe_size { + return Err(format!( + "triple {index} contains a coordinate outside 0..{universe_size}" + ) + .into()); + } } - Self { + Ok(Self { universe_size, triples, - } + }) } /// Get the universe size (q). @@ -179,8 +182,12 @@ impl Problem for ThreeDimensionalMatching { } impl crate::solvers::BruteForceProblem for ThreeDimensionalMatching { - fn dimensions(&self) -> Vec { - vec![2; self.triples.len()] + fn num_variables(&self) -> Result { + Ok(self.triples.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -196,10 +203,13 @@ crate::register_brute_force! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "three_dimensional_matching", - instance: Box::new(ThreeDimensionalMatching::new( - 3, - vec![(0, 1, 2), (1, 0, 1), (2, 2, 0), (0, 0, 0), (1, 2, 2)], - )), + instance: Box::new( + ThreeDimensionalMatching::new( + 3, + vec![(0, 1, 2), (1, 0, 1), (2, 2, 0), (0, 0, 0), (1, 2, 2)], + ) + .unwrap(), + ), optimal_config: serde_json::json!(vec![true, true, true, false, false]), optimal_value: serde_json::json!(true), }] diff --git a/src/models/set/two_dimensional_consecutive_sets.rs b/src/models/set/two_dimensional_consecutive_sets.rs index e14af706f..d0bde6b50 100644 --- a/src/models/set/two_dimensional_consecutive_sets.rs +++ b/src/models/set/two_dimensional_consecutive_sets.rs @@ -224,8 +224,12 @@ impl Problem for TwoDimensionalConsecutiveSets { } impl crate::solvers::BruteForceProblem for TwoDimensionalConsecutiveSets { - fn dimensions(&self) -> Vec { - vec![self.alphabet_size; self.alphabet_size] + fn num_variables(&self) -> Result { + Ok(self.alphabet_size) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.alphabet_size) } } diff --git a/src/random.rs b/src/random.rs index a25fbb121..bb4daeb0a 100644 --- a/src/random.rs +++ b/src/random.rs @@ -202,7 +202,7 @@ pub(crate) fn create_random_graph( .flat_map(|u| ((u + 1)..num_vertices).map(move |v| (u, v))) .filter(|_| lcg_step(&mut state) < edge_prob) .collect(); - SimpleGraph::new(num_vertices, edges) + SimpleGraph::new(num_vertices, edges).expect("generated graph endpoints are in range") } /// Generate unique integer positions on a square grid. diff --git a/src/registry/dyn_problem.rs b/src/registry/dyn_problem.rs index e5938bdc4..16f4595e7 100644 --- a/src/registry/dyn_problem.rs +++ b/src/registry/dyn_problem.rs @@ -1,11 +1,9 @@ -use serde::Serialize; use serde_json::Value; use std::any::Any; use std::collections::BTreeMap; use std::fmt; -use crate::traits::{EvaluationError, Problem}; -use crate::types::SolutionAggregate; +use crate::traits::EvaluationError; /// Format a metric for CLI- and registry-facing dynamic dispatch. /// @@ -19,13 +17,10 @@ where /// Type-erased problem interface for dynamic dispatch. /// -/// Implemented for serializable problems whose values support solution witnesses. +/// Generated for concrete variants at the registration boundary. pub trait DynProblem: Any { - /// Evaluate a configuration and return the CLI-facing metric string. - fn evaluate_dyn(&self, solution: &Value) -> Result; - /// Evaluate a candidate witness, returning `None` when it is infeasible. - /// This validates feasibility, not global optimality. - fn evaluate_witness_dyn(&self, solution: &Value) -> Result, EvaluationError>; + /// Evaluate once and return the display value and whether the configuration is feasible. + fn evaluate_dyn(&self, solution: &Value) -> Result<(String, bool), EvaluationError>; /// Evaluate a configuration and return the result as a serializable JSON value. fn evaluate_json(&self, solution: &Value) -> Result; /// Serialize the problem to a JSON value. @@ -42,57 +37,68 @@ pub trait DynProblem: Any { fn parameters_dyn(&self) -> crate::types::ProblemParameters; } -impl DynProblem for T -where - T: Problem + Serialize + 'static, - T::Solution: serde::de::DeserializeOwned, - T::Value: SolutionAggregate + fmt::Display + Serialize, -{ - fn evaluate_dyn(&self, solution: &Value) -> Result { - let solution = serde::Deserialize::deserialize(solution).map_err(|error| { - EvaluationError::InvalidConfiguration(format!("invalid solution JSON: {error}")) - })?; - Ok(format_metric(&self.evaluate(&solution)?)) - } - - fn evaluate_json(&self, solution: &Value) -> Result { - let solution = serde::Deserialize::deserialize(solution).map_err(|error| { - EvaluationError::InvalidConfiguration(format!("invalid solution JSON: {error}")) - })?; - Ok(serde_json::to_value(self.evaluate(&solution)?).expect("serialize metric failed")) - } - - fn evaluate_witness_dyn(&self, solution: &Value) -> Result, EvaluationError> { - let solution = serde::Deserialize::deserialize(solution).map_err(|error| { - EvaluationError::InvalidConfiguration(format!("invalid solution JSON: {error}")) - })?; - let value = self.evaluate(&solution)?; - Ok(T::Value::contributes_to_solution(&value, &value).then(|| format_metric(&value))) - } - - fn serialize_json(&self) -> Value { - serde_json::to_value(self).expect("serialize failed") - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn problem_name(&self) -> &'static str { - T::NAME - } - - fn variant_map(&self) -> BTreeMap { - crate::export::variant_to_map(T::variant()) - } - - fn parameter_names_dyn(&self) -> &'static [&'static str] { - T::parameter_names() - } - - fn parameters_dyn(&self) -> crate::types::ProblemParameters { - self.parameters() - } +/// Implement the existing dynamic transport boundary for a concrete problem type. +/// +/// Concrete value semantics determine feasibility; no solver capability is required. +#[macro_export] +macro_rules! impl_dyn_problem { + ($ty:ty) => { + impl $crate::registry::DynProblem for $ty { + fn evaluate_dyn( + &self, + solution: &serde_json::Value, + ) -> Result<(String, bool), $crate::traits::EvaluationError> { + let solution = serde::Deserialize::deserialize(solution).map_err(|error| { + $crate::traits::EvaluationError::InvalidConfiguration(format!( + "invalid solution JSON: {error}" + )) + })?; + let value = <$ty as $crate::traits::Problem>::evaluate(self, &solution)?; + Ok(($crate::registry::format_metric(&value), value.is_valid())) + } + + fn evaluate_json( + &self, + solution: &serde_json::Value, + ) -> Result { + let solution = serde::Deserialize::deserialize(solution).map_err(|error| { + $crate::traits::EvaluationError::InvalidConfiguration(format!( + "invalid solution JSON: {error}" + )) + })?; + Ok( + serde_json::to_value(<$ty as $crate::traits::Problem>::evaluate( + self, &solution, + )?) + .expect("serialize metric failed"), + ) + } + + fn serialize_json(&self) -> serde_json::Value { + serde_json::to_value(self).expect("serialize failed") + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn problem_name(&self) -> &'static str { + <$ty as $crate::traits::Problem>::NAME + } + + fn variant_map(&self) -> std::collections::BTreeMap { + $crate::export::variant_to_map(<$ty as $crate::traits::Problem>::variant()) + } + + fn parameter_names_dyn(&self) -> &'static [&'static str] { + <$ty as $crate::traits::Problem>::parameter_names() + } + + fn parameters_dyn(&self) -> $crate::types::ProblemParameters { + <$ty as $crate::traits::Problem>::parameters(self) + } + } + }; } /// A loaded type-erased problem. diff --git a/src/rules/acyclicpartition_ilp.rs b/src/rules/acyclicpartition_ilp.rs index df8b781e2..854909151 100644 --- a/src/rules/acyclicpartition_ilp.rs +++ b/src/rules/acyclicpartition_ilp.rs @@ -29,9 +29,12 @@ impl ReductionResult for ReductionAcyclicPartitionToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, self.n, self.n, 0) + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.n, + self.n, + 0, + )) } } @@ -134,12 +137,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source) .expect("reduction should succeed"); diff --git a/src/rules/balancedcompletebipartitesubgraph_ilp.rs b/src/rules/balancedcompletebipartitesubgraph_ilp.rs index 8c220ba38..e50b24170 100644 --- a/src/rules/balancedcompletebipartitesubgraph_ilp.rs +++ b/src/rules/balancedcompletebipartitesubgraph_ilp.rs @@ -28,8 +28,6 @@ impl ReductionResult for ReductionBCBSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) @@ -93,7 +91,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( diff --git a/src/rules/bicliquecover_bmf.rs b/src/rules/bicliquecover_bmf.rs index bcd5bd822..448a8afe0 100644 --- a/src/rules/bicliquecover_bmf.rs +++ b/src/rules/bicliquecover_bmf.rs @@ -40,8 +40,6 @@ impl ReductionResult for ReductionBicliqueCoverToBMF { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(config_bmf_to_bc(target_solution, self.m, self.n, self.k)) } } @@ -64,7 +62,7 @@ impl ReduceTo for BicliqueCover { for &(i, j) in self.graph().left_edges() { matrix[i][j] = true; } - let target = BMF::new(matrix, k); + let target = BMF::new(matrix, k).map_err(>::target_construction)?; Ok(ReductionBicliqueCoverToBMF { target, m, n, k }) } } @@ -79,7 +77,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( diff --git a/src/rules/biconnectivityaugmentation_ilp.rs b/src/rules/biconnectivityaugmentation_ilp.rs index 6fc7dd3bc..aab425f1c 100644 --- a/src/rules/biconnectivityaugmentation_ilp.rs +++ b/src/rules/biconnectivityaugmentation_ilp.rs @@ -58,15 +58,6 @@ impl ReductionResult for ReductionBiconnAugToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - if crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)? - .value - .is_none() - { - return Err(crate::rules::ExtractionError::invalid( - "target ILP assignment is infeasible", - )); - } - Ok(target_solution[..self.num_candidates] .iter() .map(|&value| value == 1) @@ -240,10 +231,11 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source) .expect("reduction should succeed"); diff --git a/src/rules/binpacking_ilp.rs b/src/rules/binpacking_ilp.rs index 24d3cc5a8..5e96b3559 100644 --- a/src/rules/binpacking_ilp.rs +++ b/src/rules/binpacking_ilp.rs @@ -41,9 +41,7 @@ impl ReductionResult for ReductionBPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows(target_solution, self.n, self.n, 0) + Ok(one_hot_decode_rows(target_solution, self.n, self.n, 0)) } } diff --git a/src/rules/bmf_bicliquecover.rs b/src/rules/bmf_bicliquecover.rs index 8f9790636..8133fb7b0 100644 --- a/src/rules/bmf_bicliquecover.rs +++ b/src/rules/bmf_bicliquecover.rs @@ -86,8 +86,6 @@ impl ReductionResult for ReductionBMFToBicliqueCover { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(config_bc_to_bmf(target_solution, self.m, self.n, self.k)) } } @@ -118,7 +116,11 @@ impl ReduceTo for BMF { } } } - let target = BicliqueCover::new(BipartiteGraph::new(m, n, edges), k); + let target = BicliqueCover::new( + BipartiteGraph::new(m, n, edges) + .map_err(>::target_construction)?, + k, + ); Ok(ReductionBMFToBicliqueCover { target, m, n, k }) } } @@ -131,7 +133,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( source, SolutionPair { diff --git a/src/rules/bmf_ilp.rs b/src/rules/bmf_ilp.rs index 8944c45e5..92a9e5970 100644 --- a/src/rules/bmf_ilp.rs +++ b/src/rules/bmf_ilp.rs @@ -29,8 +29,6 @@ impl ReductionResult for ReductionBMFToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let b = (0..self.m) .map(|i| { (0..self.k) @@ -129,7 +127,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/bottlenecktravelingsalesman_ilp.rs b/src/rules/bottlenecktravelingsalesman_ilp.rs index 6951b619a..a0988f820 100644 --- a/src/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/rules/bottlenecktravelingsalesman_ilp.rs @@ -27,13 +27,6 @@ impl ReductionResult for ReductionBTSPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.is_valid() { - return Err(crate::rules::ExtractionError::invalid( - "target ILP assignment is infeasible", - )); - } let n = self.num_vertices; Ok((0..self.num_edges) .map(|edge| { @@ -199,9 +192,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/boundedcomponentspanningforest_ilp.rs b/src/rules/boundedcomponentspanningforest_ilp.rs index 96c32f915..e0cbce555 100644 --- a/src/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/rules/boundedcomponentspanningforest_ilp.rs @@ -31,9 +31,7 @@ impl ReductionResult for ReductionBCSFToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows(target_solution, self.n, self.k, 0) + Ok(one_hot_decode_rows(target_solution, self.n, self.k, 0)) } } @@ -193,11 +191,12 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source) .expect("reduction should succeed"); diff --git a/src/rules/capacityassignment_ilp.rs b/src/rules/capacityassignment_ilp.rs index 1b348b59a..28304aff4 100644 --- a/src/rules/capacityassignment_ilp.rs +++ b/src/rules/capacityassignment_ilp.rs @@ -38,14 +38,12 @@ impl ReductionResult for ReductionCAToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_links, self.num_capacities, 0, - ) + )) } } @@ -126,7 +124,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/circuit_ilp.rs b/src/rules/circuit_ilp.rs index 6fa63d452..a86bda996 100644 --- a/src/rules/circuit_ilp.rs +++ b/src/rules/circuit_ilp.rs @@ -40,15 +40,6 @@ impl ReductionResult for ReductionCircuitToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - if crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)? - .value - .is_none() - { - return Err(crate::rules::ExtractionError::invalid( - "target ILP assignment is infeasible", - )); - } - Ok({ self.source_variables .iter() diff --git a/src/rules/circuit_sat.rs b/src/rules/circuit_sat.rs index 34811903a..93a36c07a 100644 --- a/src/rules/circuit_sat.rs +++ b/src/rules/circuit_sat.rs @@ -293,8 +293,6 @@ impl ReductionResult for ReductionCircuitSATToSAT { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.source_var_count].to_vec()) } } diff --git a/src/rules/circuit_spinglass.rs b/src/rules/circuit_spinglass.rs index 92ee614d4..0bc7c3997 100644 --- a/src/rules/circuit_spinglass.rs +++ b/src/rules/circuit_spinglass.rs @@ -230,14 +230,6 @@ impl ReductionResult for ReductionCircuitToSG { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "SpinGlass energy does not meet the circuit zero-penalty threshold", - )); - } - Ok(self .source_variables .iter() diff --git a/src/rules/closeststring_ilp.rs b/src/rules/closeststring_ilp.rs index 1f7d2a922..85186bc69 100644 --- a/src/rules/closeststring_ilp.rs +++ b/src/rules/closeststring_ilp.rs @@ -55,26 +55,12 @@ impl ReductionResult for ReductionClosestStringToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - let q = self.alphabet_size; - let mut center = Vec::with_capacity(self.string_length); - for position in 0..self.string_length { - let block = &target_solution[position * q..(position + 1) * q]; - let mut selected = block.iter().enumerate().filter(|(_, value)| **value == 1); - let symbol = selected.next().map(|(symbol, _)| symbol).ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "center position {position} has no selected symbol" - )) - })?; - if selected.next().is_some() || block.iter().any(|&value| value > 1) { - return Err(crate::rules::ExtractionError::invalid(format!( - "center position {position} is not one-hot" - ))); - } - center.push(symbol); - } - Ok(center) + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.string_length, + self.alphabet_size, + 0, + )) } } @@ -149,7 +135,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/closestsubstring_ilp.rs b/src/rules/closestsubstring_ilp.rs index 65416c54c..de280add8 100644 --- a/src/rules/closestsubstring_ilp.rs +++ b/src/rules/closestsubstring_ilp.rs @@ -75,49 +75,31 @@ impl ReductionResult for ReductionClosestSubstringToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let q = self.alphabet_size; let ell = self.substring_length; let y_base = q * ell; let mut out = Vec::with_capacity(ell + self.window_counts.len()); - for position in 0..ell { - let block = &target_solution[position * q..(position + 1) * q]; - out.push(decode_one_hot(block, "center position", position)?); - } + out.extend(crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + ell, + q, + 0, + )); for (string, &window_count) in self.window_counts.iter().enumerate() { let start = y_base + self.window_offsets[string]; - out.push(decode_one_hot( - &target_solution[start..start + window_count], - "string window", - string, - )?); + out.extend(crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + 1, + window_count, + start, + )); } Ok(out) } } -fn decode_one_hot( - block: &[i64], - block_name: &str, - block_index: usize, -) -> crate::rules::ExtractionResult { - let mut selected = block.iter().enumerate().filter(|(_, value)| **value == 1); - let index = selected.next().map(|(index, _)| index).ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "{block_name} {block_index} has no selected value" - )) - })?; - if selected.next().is_some() || block.iter().any(|&value| value > 1) { - return Err(crate::rules::ExtractionError::invalid(format!( - "{block_name} {block_index} is not one-hot" - ))); - } - Ok(index) -} - #[reduction( transform = exact { num_vars = "alphabet_size * substring_length + total_num_windows + 1", @@ -166,11 +148,8 @@ impl ReduceTo> for ClosestSubstring { } // Tight upper bound on R: the worst-case Hamming distance over a - // length-ell window is at most ell. Added as a single-term `<=` - // constraint so the solver's bound-tightening pass (which scans for - // exactly this pattern) picks it up. Without this, R defaults to the - // full i64 domain, which severely degrades HiGHS performance even on - // tiny instances. + // length-ell window is at most ell. Restricting R to this range + // preserves every optimal solution. constraints.push(LinearConstraint::le(vec![(r_idx, 1)], ell_i64)); // Window-choice constraints: exactly one window per input string. diff --git a/src/rules/closestvectorproblem_qubo.rs b/src/rules/closestvectorproblem_qubo.rs index 3efa14979..a5f16848f 100644 --- a/src/rules/closestvectorproblem_qubo.rs +++ b/src/rules/closestvectorproblem_qubo.rs @@ -41,28 +41,17 @@ impl ReductionResult for ReductionCVPToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - self.encodings .iter() .map(|encoding| { - let offset = encoding.weights.iter().enumerate().try_fold( - 0_i64, - |offset, (index, &weight)| { - if target_solution[encoding.start + index] { - offset.checked_add(weight) - } else { - Some(offset) - } - }, - ); - offset - .and_then(|offset| encoding.lower.checked_add(offset)) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "decoded closest-vector coefficient overflows i64", - ) - }) + let offset: i64 = encoding + .weights + .iter() + .enumerate() + .filter(|(index, _)| target_solution[encoding.start + index]) + .map(|(_, &weight)| weight) + .sum(); + Ok(encoding.lower + offset) }) .collect() } @@ -110,9 +99,7 @@ fn determinant(matrix: &[Vec]) -> Result } fn coefficient_bounds(problem: &Source) -> Result, crate::rules::ReductionError> { - let rows = problem - .independent_rows() - .map_err(crate::rules::ReductionError::construction::)?; + let rows = problem.independent_rows(); let size = problem.num_basis_vectors(); if size == 0 { return Ok(Vec::new()); @@ -293,7 +280,7 @@ impl ReduceTo> for ClosestVectorProblem { .map(move |&weight| (coefficient, weight)) }) .collect::>(); - let mut integer_matrix = vec![vec![0_i64; total_bits]; total_bits]; + let mut integer_matrix = vec![std::collections::BTreeMap::new(); total_bits]; for u in 0..total_bits { let (coefficient_u, weight_u) = bit_terms[u]; let quadratic = gram[coefficient_u][coefficient_u] @@ -304,22 +291,27 @@ impl ReduceTo> for ClosestVectorProblem { .checked_mul(weight_u) .and_then(|value| value.checked_mul(2)) .ok_or_else(|| overflow("computing a closest-vector QUBO diagonal"))?; - integer_matrix[u][u] = quadratic - .checked_add(linear_term) - .ok_or_else(|| overflow("computing a closest-vector QUBO diagonal"))?; + integer_matrix[u].insert( + u, + quadratic + .checked_add(linear_term) + .ok_or_else(|| overflow("computing a closest-vector QUBO diagonal"))?, + ); - for v in (u + 1)..total_bits { - let (coefficient_v, weight_v) = bit_terms[v]; - integer_matrix[u][v] = gram[coefficient_u][coefficient_v] + for (v, &(coefficient_v, weight_v)) in bit_terms.iter().enumerate().skip(u + 1) { + let coefficient = gram[coefficient_u][coefficient_v] .checked_mul(weight_u) .and_then(|value| value.checked_mul(weight_v)) .and_then(|value| value.checked_mul(2)) .ok_or_else(|| overflow("computing a closest-vector QUBO interaction"))?; + if coefficient != 0 { + integer_matrix[u].insert(v, coefficient); + } } } Ok(ReductionCVPToQUBO { - target: QUBO::from_matrix(integer_matrix) + target: QUBO::from_rows(integer_matrix) .map_err(crate::rules::ReductionError::construction::)?, encodings, }) diff --git a/src/rules/clustering_ilp.rs b/src/rules/clustering_ilp.rs index bb3149ae0..3e622f65f 100644 --- a/src/rules/clustering_ilp.rs +++ b/src/rules/clustering_ilp.rs @@ -30,14 +30,12 @@ impl ReductionResult for ReductionClusteringToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_elements, self.num_clusters, 0, - ) + )) } } @@ -109,7 +107,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/coloring_ilp.rs b/src/rules/coloring_ilp.rs index 37c6f5944..074d88d44 100644 --- a/src/rules/coloring_ilp.rs +++ b/src/rules/coloring_ilp.rs @@ -48,9 +48,12 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows(target_solution, self.num_vertices, self.num_colors, 0) + Ok(one_hot_decode_rows( + target_solution, + self.num_vertices, + self.num_colors, + 0, + )) } } @@ -139,7 +142,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::with_k(SimpleGraph::new(n, edges), 3); + let source = KColoring::::with_k(SimpleGraph::new(n, edges).unwrap(), 3); crate::example_db::specs::rule_example_with_witness::<_, ILP>( source, SolutionPair { diff --git a/src/rules/coloring_qubo.rs b/src/rules/coloring_qubo.rs index c29893620..60e8963eb 100644 --- a/src/rules/coloring_qubo.rs +++ b/src/rules/coloring_qubo.rs @@ -33,34 +33,21 @@ impl ReductionResult for ReductionKColoringToQUBO { &self.target } - /// Decode one-hot: for each vertex, find which color bit is 1. + /// Decode a target witness at `feasible_energy` into a proper coloring. + /// At that energy all nonnegative penalties vanish, including one-hot. + /// An optimum above the threshold means the source is uncolorable and + /// is interpreted through `extract_value` before witness extraction. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target QUBO configuration does not certify a proper coloring", - )); - } - - (0..self.num_vertices) + Ok((0..self.num_vertices) .map(|vertex| { - let mut selected = (0..self.num_colors) - .filter(|&color| target_solution[vertex * self.num_colors + color]); - match (selected.next(), selected.next()) { - (Some(color), None) => Ok(color), - (None, _) => Err(crate::rules::ExtractionError::invalid(format!( - "assignment row {vertex} has no selected color" - ))), - (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( - "assignment row {vertex} has multiple selected colors" - ))), - } + (0..self.num_colors) + .find(|&color| target_solution[vertex * self.num_colors + color]) + .unwrap() }) - .collect() + .collect()) } } @@ -125,7 +112,7 @@ fn reduce_kcoloring_to_qubo( .checked_mul(4) .ok_or_else(|| overflow("computing a one-hot interaction coefficient"))?; - let mut matrix = vec![vec![0i64; nq]; nq]; + let mut matrix = vec![std::collections::BTreeMap::new(); nq]; // Twice the former half-integral objective keeps every coefficient integral. // One-hot penalty: 2P*sum_v (1 - sum_c x_{v,c})^2 @@ -136,7 +123,8 @@ fn reduce_kcoloring_to_qubo( for c in 0..k { let idx = v * k + c; // Diagonal: -2P - matrix[idx][idx] = matrix[idx][idx] + let coefficient = matrix[idx].entry(idx).or_insert(0i64); + *coefficient = coefficient .checked_add(diagonal_penalty) .ok_or_else(|| overflow("adding a coloring diagonal coefficient"))?; } @@ -145,7 +133,8 @@ fn reduce_kcoloring_to_qubo( for c2 in (c1 + 1)..k { let idx1 = v * k + c1; let idx2 = v * k + c2; - matrix[idx1][idx2] = matrix[idx1][idx2] + let coefficient = matrix[idx1].entry(idx2).or_insert(0i64); + *coefficient = coefficient .checked_add(one_hot_interaction) .ok_or_else(|| overflow("adding a one-hot interaction coefficient"))?; } @@ -162,14 +151,15 @@ fn reduce_kcoloring_to_qubo( } else { (idx_v, idx_u) }; - matrix[i][j] = matrix[i][j] + let coefficient = matrix[i].entry(j).or_insert(0i64); + *coefficient = coefficient .checked_add(penalty) .ok_or_else(|| overflow("adding an edge-conflict coefficient"))?; } } Ok(ReductionKColoringToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { + target: QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::, QUBO>( message, ) @@ -219,7 +209,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::with_k(SimpleGraph::new(n, edges), 3); + let source = KColoring::::with_k(SimpleGraph::new(n, edges).unwrap(), 3); crate::example_db::specs::rule_example_with_witness::<_, QUBO>( source, SolutionPair { diff --git a/src/rules/consecutiveblockminimization_ilp.rs b/src/rules/consecutiveblockminimization_ilp.rs index ca25d2c2f..b1d8ce7c1 100644 --- a/src/rules/consecutiveblockminimization_ilp.rs +++ b/src/rules/consecutiveblockminimization_ilp.rs @@ -28,9 +28,12 @@ impl ReductionResult for ReductionCBMToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode(target_solution, self.num_cols, self.num_cols, 0) + Ok(one_hot_decode( + target_solution, + self.num_cols, + self.num_cols, + 0, + )) } } @@ -128,7 +131,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs index 0941ff0a7..bafcf8a29 100644 --- a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs +++ b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs @@ -29,9 +29,12 @@ impl ReductionResult for ReductionCOMAToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode(target_solution, self.num_cols, self.num_cols, 0) + Ok(one_hot_decode( + target_solution, + self.num_cols, + self.num_cols, + 0, + )) } } diff --git a/src/rules/consecutiveonessubmatrix_ilp.rs b/src/rules/consecutiveonessubmatrix_ilp.rs index d279e0913..1f094b823 100644 --- a/src/rules/consecutiveonessubmatrix_ilp.rs +++ b/src/rules/consecutiveonessubmatrix_ilp.rs @@ -26,8 +26,6 @@ impl ReductionResult for ReductionCOSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Output the selection bits s_c (first num_cols variables) target_solution[..self.num_cols] @@ -217,7 +215,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source).expect("reduction should succeed"); let ilp_solver = crate::solvers::ILPSolver::new(); diff --git a/src/rules/consistencyofdatabasefrequencytables_ilp.rs b/src/rules/consistencyofdatabasefrequencytables_ilp.rs index 71710293c..eaafb91b4 100644 --- a/src/rules/consistencyofdatabasefrequencytables_ilp.rs +++ b/src/rules/consistencyofdatabasefrequencytables_ilp.rs @@ -95,30 +95,17 @@ impl ReductionResult for ReductionCDFTToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let mut source_solution = Vec::with_capacity(self.source.num_assignment_variables()); for object in 0..self.source.num_objects() { for (attribute, &domain_size) in self.source.attribute_domains().iter().enumerate() { - let mut selected = (0..domain_size).filter(|&candidate| { - target_solution[self.assignment_var_index(object, attribute, candidate)] - == 1 - }); - let value = match (selected.next(), selected.next()) { - (Some(value), None) => value, - (None, _) => { - return Err(crate::rules::ExtractionError::invalid(format!( - "object {object}, attribute {attribute} has no selected value" - ))) - } - (Some(_), Some(_)) => { - return Err(crate::rules::ExtractionError::invalid(format!( - "object {object}, attribute {attribute} has multiple selected values" - ))) - } - }; + let value = (0..domain_size) + .filter(|&candidate| { + target_solution[self.assignment_var_index(object, attribute, candidate)] + == 1 + }) + .sum(); source_solution.push(value); } } @@ -235,7 +222,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/decisionmaximumindependentset_integralflowbundles.rs b/src/rules/decisionmaximumindependentset_integralflowbundles.rs index 7fdeab6b4..253bc8d32 100644 --- a/src/rules/decisionmaximumindependentset_integralflowbundles.rs +++ b/src/rules/decisionmaximumindependentset_integralflowbundles.rs @@ -31,13 +31,6 @@ impl ReductionResult for ReductionDecisionMISToIFB { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let feasible = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !feasible.0 { - return Err(crate::rules::ExtractionError::invalid( - "target flow must satisfy conservation, bundle capacities, and the requirement", - )); - } Ok((0..self.num_source_vertices) .map(|i| target_solution[2 * i + 1] == 1) .collect()) @@ -111,13 +104,15 @@ impl ReduceTo for Decision>::target_construction)?, 0, sink, bundles, capacities, requirement, - ); + ) + .map_err(>::target_construction)?; Ok(ReductionDecisionMISToIFB { target, num_source_vertices: n, @@ -133,7 +128,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::reduce_to(&source) diff --git a/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs b/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs index 60d57a4cb..4d6e656d2 100644 --- a/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs +++ b/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs @@ -32,13 +32,6 @@ impl ReductionResult for ReductionDecisionMinimumDominatingSetToMinimumSumMultic &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target placement does not certify a dominating set within the source bound", - )); - } // Original vertices precede the auxiliary isolated vertices. Ok(target_solution[..self.source_num_vertices].to_vec()) } @@ -73,11 +66,16 @@ impl ReduceTo> let n = source_graph.num_vertices(); let (target_n, centers, threshold) = multicenter_parameters(n, *self.bound())?; let target = MinimumSumMulticenter::new( - SimpleGraph::new(target_n, source_graph.edges()), + SimpleGraph::new(target_n, source_graph.edges()).map_err( + >>::target_construction, + )?, vec![1i64; target_n], vec![1i64; source_graph.num_edges()], centers, - ); + ) + .map_err( + >>::target_construction, + )?; Ok( ReductionDecisionMinimumDominatingSetToMinimumSumMulticenter { target, @@ -131,9 +129,11 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target placement does not certify a dominating set: radius must be at most one", - )); - } Ok(target_solution[..self.source_num_vertices].to_vec()) } } @@ -73,11 +66,14 @@ impl ReduceTo> let n = source_graph.num_vertices(); let (target_n, centers) = multicenter_parameters(n, *self.bound())?; let target = MinMaxMulticenter::new( - SimpleGraph::new(target_n, source_graph.edges()), + SimpleGraph::new(target_n, source_graph.edges()).map_err( + >>::target_construction, + )?, vec![One; target_n], vec![One; source_graph.num_edges()], centers, - ); + ) + .map_err(>>::target_construction)?; Ok(ReductionDecisionMinimumDominatingSetToMinMaxMulticenter { target, source_num_vertices: n, @@ -122,9 +118,11 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>` model. +//! on the unit-weight `Decision>` model. use crate::models::decision::Decision; use crate::models::graph::{HamiltonianCircuit, MinimumVertexCover}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::Problem; +use crate::types::One; use std::collections::BTreeSet; #[derive(Debug, Clone)] enum ConstructionKind { - FixedYes { source_cover: Vec }, - FixedNo, + Fixed { source_cover: Vec }, Theorem(TheoremConstruction), } @@ -184,24 +183,12 @@ impl TheoremConstruction { fn decode_solution( &self, - target_problem: &HamiltonianCircuit, - target_solution: &Vec, + target_solution: &[usize], ) -> crate::rules::ExtractionResult> { Ok({ let mut source_cover = vec![false; self.num_source_vertices]; - if !target_problem.evaluate(target_solution)?.0 { - return Err(crate::rules::ExtractionError::invalid( - "target configuration is not a Hamiltonian circuit", - )); - } - let mut positions = vec![usize::MAX; target_solution.len()]; for (idx, &vertex) in target_solution.iter().enumerate() { - if vertex >= positions.len() || positions[vertex] != usize::MAX { - return Err(crate::rules::ExtractionError::invalid( - "target circuit contains an invalid or repeated vertex", - )); - } positions[vertex] = idx; } @@ -222,19 +209,12 @@ impl TheoremConstruction { } } - let selected_count = source_cover.iter().filter(|&&x| x).count(); - if selected_count != self.selector_count || !self.covers_all_edges(&source_cover) { - return Err(crate::rules::ExtractionError::invalid( - "target circuit does not encode a source vertex cover of the required size", - )); - } - source_cover }) } } -/// Result of reducing Decision> to +/// Result of reducing Decision> to /// HamiltonianCircuit. #[derive(Debug, Clone)] pub struct ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { @@ -246,8 +226,7 @@ impl ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { #[cfg(any(test, feature = "example-db"))] fn build_target_witness(&self, source_cover: &[bool]) -> Vec { match &self.construction { - ConstructionKind::FixedYes { .. } => vec![0, 1, 2], - ConstructionKind::FixedNo => Vec::new(), + ConstructionKind::Fixed { .. } => vec![0, 1, 2], ConstructionKind::Theorem(construction) => { construction.build_target_witness(source_cover) } @@ -256,7 +235,7 @@ impl ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { } impl ReductionResult for ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { - type Source = Decision>; + type Source = Decision>; type Target = HamiltonianCircuit; fn target_problem(&self) -> &Self::Target { @@ -267,26 +246,11 @@ impl ReductionResult for ReductionDecisionMinimumVertexCoverToHamiltonianCircuit &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ match &self.construction { - ConstructionKind::FixedYes { source_cover } => { - if self.target.evaluate(target_solution)?.0 { - source_cover.clone() - } else { - return Err(crate::rules::ExtractionError::invalid( - "target configuration is not the fixed Hamiltonian circuit", - )); - } - } - ConstructionKind::FixedNo => { - return Err(crate::rules::ExtractionError::invalid( - "the fixed negative target instance has no extractable witness", - )) - } + ConstructionKind::Fixed { source_cover } => source_cover.clone(), ConstructionKind::Theorem(construction) => { - construction.decode_solution(&self.target, target_solution)? + construction.decode_solution(target_solution)? } } }) @@ -313,30 +277,21 @@ fn insert_edge(edges: &mut BTreeSet<(usize, usize)>, a: usize, b: usize) { num_edges = "the construction size depends on the decision threshold, which is not a problem parameter", } )] -impl ReduceTo> for Decision> { +impl ReduceTo> for Decision> { type Result = ReductionDecisionMinimumVertexCoverToHamiltonianCircuit; fn reduce_to(&self) -> Result { - let weights = self.inner().weights(); - if weights.iter().any(|&weight| weight != 1) { - return Err(crate::rules::ReductionError::invalid_target::< - Decision>, - HamiltonianCircuit, - >( - "Garey-Johnson construction requires unit vertex weights" - )); - } - let num_source_vertices = self.inner().graph().num_vertices(); let raw_bound = *self.bound(); if raw_bound < 0 { return Ok(ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { target: HamiltonianCircuit::new(SimpleGraph::path(3)), - construction: ConstructionKind::FixedNo, + construction: ConstructionKind::Fixed { + source_cover: vec![false; num_source_vertices], + }, }); } - let k = self.k(); let edges = normalize_edges(self.inner().graph().edges()); let mut incident_edges = vec![Vec::new(); num_source_vertices]; for (edge_idx, &(u, v)) in edges.iter().enumerate() { @@ -352,27 +307,30 @@ impl ReduceTo> for Decision= active_count { + if i128::from(raw_bound) >= active_count as i128 { let mut source_cover = vec![false; num_source_vertices]; for vertex in active_vertices { source_cover[vertex] = true; } return Ok(ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { target: HamiltonianCircuit::new(SimpleGraph::cycle(3)), - construction: ConstructionKind::FixedYes { source_cover }, + construction: ConstructionKind::Fixed { source_cover }, }); } - if k == 0 { + if raw_bound == 0 { return Ok(ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { target: HamiltonianCircuit::new(SimpleGraph::path(3)), - construction: ConstructionKind::FixedNo, + construction: ConstructionKind::Fixed { + source_cover: vec![false; num_source_vertices], + }, }); } let construction = TheoremConstruction { num_source_vertices, - selector_count: k, + selector_count: usize::try_from(raw_bound) + .expect("nonnegative bound is smaller than the active vertex count"), edges, incident_edges, }; @@ -426,7 +384,7 @@ impl ReduceTo> for Decision>, + Decision>, HamiltonianCircuit, >("active source vertex has no Hamiltonian gadget path endpoints") })?; @@ -436,10 +394,13 @@ impl ReduceTo> for Decision>>::target_construction)?, + ); Ok(ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { target, @@ -457,7 +418,11 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_vertices; // Decode one-hot assignment: permutation[k] = v where x_{v,k} = 1 - one_hot_decode(target_solution, n, n, 0)? + one_hot_decode(target_solution, n, n, 0) }) } } @@ -117,10 +115,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec1->2->3 - let source = DirectedHamiltonianPath::new(crate::topology::DirectedGraph::new( - 4, - vec![(0, 1), (1, 2), (2, 3)], - )); + let source = DirectedHamiltonianPath::new( + crate::topology::DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), + ); crate::example_db::specs::rule_example_via_ilp::<_, bool>(source) }, }] diff --git a/src/rules/directedtwocommodityintegralflow_ilp.rs b/src/rules/directedtwocommodityintegralflow_ilp.rs index 6e644d21f..e8e75c166 100644 --- a/src/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/rules/directedtwocommodityintegralflow_ilp.rs @@ -41,8 +41,6 @@ impl ReductionResult for ReductionD2CIFToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(&target_solution[..2 * self.num_arcs]) } } @@ -175,7 +173,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec Vec(source) }, }] diff --git a/src/rules/disjointconnectingpaths_ilp.rs b/src/rules/disjointconnectingpaths_ilp.rs index 5d1269c13..b23995f24 100644 --- a/src/rules/disjointconnectingpaths_ilp.rs +++ b/src/rules/disjointconnectingpaths_ilp.rs @@ -40,8 +40,6 @@ impl ReductionResult for ReductionDCPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let mut result = vec![false; self.edges.len()]; for (k, &(source, sink)) in self.terminal_pairs.iter().enumerate() { let offset = k * self.num_edge_vars_per_commodity; @@ -71,12 +69,7 @@ impl ReductionResult for ReductionDCPToILP { } } let mut vertex = sink; - while vertex != source { - let (previous, edge) = predecessor[vertex].ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "commodity flow does not connect its terminal pair", - ) - })?; + while let Some((previous, edge)) = predecessor[vertex] { result[edge] = true; vertex = previous; } @@ -196,9 +189,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/ensemblecomputation_ilp.rs b/src/rules/ensemblecomputation_ilp.rs index 2ed359613..5c342c690 100644 --- a/src/rules/ensemblecomputation_ilp.rs +++ b/src/rules/ensemblecomputation_ilp.rs @@ -42,30 +42,20 @@ impl ReductionResult for ReductionEnsembleComputationToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; let mut config = Vec::with_capacity(2 * self.budget); - let mut inactive = false; for step in 0..self.budget { let active = target_solution[self.activity_base + step]; if active == 0 { - inactive = true; - continue; - } - if active != 1 || inactive { - return Err(crate::rules::ExtractionError::invalid( - "active ensemble-operation slots must form a binary prefix", - )); + break; } for left in [true, false] { - let selected = (0..self.universe_size + step) - .filter(|&operand| target_solution[self.selector_var(left, step, operand)] == 1) - .collect::>(); - if selected.len() != 1 { - return Err(crate::rules::ExtractionError::invalid( - "each active ensemble operation must select exactly one operand per side", - )); - } - config.push(selected[0]); + config.push( + (0..self.universe_size + step) + .filter(|&operand| { + target_solution[self.selector_var(left, step, operand)] == 1 + }) + .sum(), + ); } } let filler = if self.universe_size >= 2 { diff --git a/src/rules/eulerianpath_ilp.rs b/src/rules/eulerianpath_ilp.rs index ce8ec318b..e1cfb8ebd 100644 --- a/src/rules/eulerianpath_ilp.rs +++ b/src/rules/eulerianpath_ilp.rs @@ -68,56 +68,30 @@ impl ReductionResult for ReductionEulerianPathToILP { /// /// Reads the unique active start arc (`s_a = 1`) and walks the active /// successor relation (`y_{a,b} = 1`) one step at a time, producing an arc - /// permutation of length `m`. Malformed assignments return an extraction - /// error instead of fabricating an ordering. + /// permutation of length `m` under the target path constraints. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let m = self.num_arcs; if m == 0 { return Ok(Vec::new()); } - // Find the unique active start arc. - let mut current = match (0..m).find(|&a| target_solution[self.s_idx(a)] == 1) { - Some(a) => a, - None => { - return Err(crate::rules::ExtractionError::invalid( - "ILP witness has no active Eulerian-path start arc", - )); - } - }; - - // Walk the active successor relation, recording each visited arc. + let mut current = (0..m) + .filter(|&a| target_solution[self.s_idx(a)] == 1) + .sum(); let mut order = Vec::with_capacity(m); - let mut visited = vec![false; m]; - order.push(current); - visited[current] = true; - - for _ in 1..m { - let next = self + for _ in 0..m { + order.push(current); + current = self .pairs .iter() .enumerate() - .find(|&(k, &(a, _))| a == current && target_solution[k] == 1) - .map(|(_, &(_, b))| b); - - match next { - Some(b) if !visited[b] => { - order.push(b); - visited[b] = true; - current = b; - } - _ => { - return Err(crate::rules::ExtractionError::invalid(format!( - "ILP witness has no unvisited successor for arc {current}", - ))); - } - } + .filter(|&(k, &(a, _))| a == current && target_solution[k] == 1) + .map(|(_, &(_, b))| b) + .sum(); } order }) @@ -251,8 +225,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec1->2->0->1. - let source = - EulerianPath::new(DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)])); + let source = EulerianPath::new( + DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)]).unwrap(), + ); crate::example_db::specs::rule_example_via_ilp::<_, i64>(source) }, }] diff --git a/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs b/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs index ace09c92b..0a1222112 100644 --- a/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs +++ b/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs @@ -22,8 +22,6 @@ impl ReductionResult for ReductionX3CToAlgebraicEquationsOverGF2 { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -77,7 +75,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( - ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]), + ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]).unwrap(), SolutionPair { source_config: serde_json::json!(vec![true, true, false]), target_config: serde_json::json!(vec![true, true, false]), diff --git a/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs b/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs index f87ba0c2a..eeba2349f 100644 --- a/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs +++ b/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs @@ -98,14 +98,6 @@ impl ReductionResult for ReductionX3CToBoundedDiameterSpanningTree { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target edge selection is not a feasible bounded-diameter spanning tree", - )); - } - Ok({ let m = self.source_num_subsets; let root_to_set_offset = 2; @@ -134,7 +126,7 @@ impl ReduceTo> for ExactCoverBy3Se // Two isolated vertices have no spanning tree. No certificate can // pass validation, so the ordinary extractor is never reached. return Ok(ReductionX3CToBoundedDiameterSpanningTree { - target: BoundedDiameterSpanningTree::new(SimpleGraph::empty(2), vec![], 1, 4), + target: BoundedDiameterSpanningTree::new(SimpleGraph::empty(2), vec![], 1, 4).map_err(>>::target_construction)?, source_num_subsets: m, }); } @@ -185,8 +177,10 @@ impl ReduceTo> for ExactCoverBy3Se let diameter_bound: usize = 4; - let graph = SimpleGraph::new(num_vertices, edges); - let target = BoundedDiameterSpanningTree::new(graph, weights, weight_bound, diameter_bound); + let graph = SimpleGraph::new(num_vertices, edges).map_err( + >>::target_construction, + )?; + let target = BoundedDiameterSpanningTree::new(graph, weights, weight_bound, diameter_bound).map_err(>>::target_construction)?; Ok(ReductionX3CToBoundedDiameterSpanningTree { target, @@ -205,7 +199,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -82,7 +80,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/exactcoverby3sets_maximumsetpacking.rs b/src/rules/exactcoverby3sets_maximumsetpacking.rs index 60536abc0..fa4dbbe4e 100644 --- a/src/rules/exactcoverby3sets_maximumsetpacking.rs +++ b/src/rules/exactcoverby3sets_maximumsetpacking.rs @@ -33,8 +33,6 @@ impl ReductionResult for ReductionXC3SToMaximumSetPacking { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -75,7 +73,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/exactcoverby3sets_minimumaxiomset.rs b/src/rules/exactcoverby3sets_minimumaxiomset.rs index 5c9827351..708fabb91 100644 --- a/src/rules/exactcoverby3sets_minimumaxiomset.rs +++ b/src/rules/exactcoverby3sets_minimumaxiomset.rs @@ -33,8 +33,6 @@ impl ReductionResult for ReductionXC3SToMinimumAxiomSet { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let set_offset = self.source_universe_size; (0..self.source_num_subsets) @@ -68,7 +66,8 @@ impl ReduceTo for ExactCoverBy3Sets { } let target = - MinimumAxiomSet::new(num_sentences, (0..num_sentences).collect(), implications); + MinimumAxiomSet::new(num_sentences, (0..num_sentences).collect(), implications) + .map_err(>::target_construction)?; Ok(ReductionXC3SToMinimumAxiomSet { target, @@ -88,7 +87,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( source, SolutionPair { diff --git a/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs b/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs index eb2846373..5b0b0015c 100644 --- a/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs +++ b/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs @@ -28,8 +28,6 @@ impl ReductionResult for ReductionXC3SToMinimumFaultDetectionTestSet { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|row| row[0]).collect()) } } @@ -65,7 +63,8 @@ impl ReduceTo for ExactCoverBy3Sets { arcs, (0..num_inputs).collect(), vec![output], - ), + ) + .map_err(>::target_construction)?, }) } } @@ -77,7 +76,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( source, SolutionPair { diff --git a/src/rules/exactcoverby3sets_staffscheduling.rs b/src/rules/exactcoverby3sets_staffscheduling.rs index 3721b7fb9..f82b0d341 100644 --- a/src/rules/exactcoverby3sets_staffscheduling.rs +++ b/src/rules/exactcoverby3sets_staffscheduling.rs @@ -37,8 +37,6 @@ impl ReductionResult for ReductionXC3SToStaffScheduling { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&count| count > 0).collect()) } } @@ -83,7 +81,8 @@ impl ReduceTo for ExactCoverBy3Sets { schedules, requirements, num_workers, - ); + ) + .map_err(>::target_construction)?; Ok(ReductionXC3SToStaffScheduling { target }) } @@ -99,7 +98,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( source, diff --git a/src/rules/exactcoverby3sets_subsetproduct.rs b/src/rules/exactcoverby3sets_subsetproduct.rs index 4084f1a40..92e3cd91c 100644 --- a/src/rules/exactcoverby3sets_subsetproduct.rs +++ b/src/rules/exactcoverby3sets_subsetproduct.rs @@ -30,8 +30,6 @@ impl ReductionResult for ReductionX3CToSubsetProduct { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -75,7 +73,8 @@ impl ReduceTo for ExactCoverBy3Sets { let target = product_biguint(primes.iter().copied()); Ok(ReductionX3CToSubsetProduct { - target: SubsetProduct::new(values, target), + target: SubsetProduct::new(values, target) + .map_err(>::target_construction)?, }) } } @@ -88,7 +87,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( - ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]), + ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]).unwrap(), SolutionPair { source_config: serde_json::json!(vec![true, true, false]), target_config: serde_json::json!(vec![true, true, false]), diff --git a/src/rules/expectedretrievalcost_ilp.rs b/src/rules/expectedretrievalcost_ilp.rs index a2d76cefb..f6968422d 100644 --- a/src/rules/expectedretrievalcost_ilp.rs +++ b/src/rules/expectedretrievalcost_ilp.rs @@ -20,18 +20,6 @@ use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode_rows}; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Compute the latency distance between sectors on a circular device. -/// -/// Returns the number of sectors between source and target (not counting source itself), -/// wrapping around. This matches the `latency_distance` function in the model. -fn latency_distance(num_sectors: usize, source: usize, target: usize) -> usize { - if source < target { - target - source - 1 - } else { - num_sectors - source + target - 1 - } -} - /// Result of reducing ExpectedRetrievalCost to ILP. /// /// Variable layout: @@ -70,9 +58,12 @@ impl ReductionResult for ReductionERCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows(target_solution, self.num_records, self.num_sectors, 0) + Ok(one_hot_decode_rows( + target_solution, + self.num_records, + self.num_sectors, + 0, + )) } } @@ -135,7 +126,7 @@ impl ReduceTo> for ExpectedRetrievalCost { for s in 0..num_sectors { for r2 in 0..num_records { for s2 in 0..num_sectors { - let lat = latency_distance(num_sectors, s, s2) as f64; + let lat = self.latency_distance(s, s2) as f64; if lat > 0.0 { let coeff = lat * probabilities[r] * probabilities[r2]; if coeff.abs() > 0.0 { diff --git a/src/rules/factoring_circuit.rs b/src/rules/factoring_circuit.rs index 330406035..f62be444c 100644 --- a/src/rules/factoring_circuit.rs +++ b/src/rules/factoring_circuit.rs @@ -47,14 +47,6 @@ impl ReductionResult for ReductionFactoringToCircuit { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target assignment does not satisfy the multiplication circuit", - )); - } - Ok({ let var_names = self.target.variable_names(); @@ -69,21 +61,16 @@ impl ReductionResult for ReductionFactoringToCircuit { names .iter() .enumerate() - .try_fold(BigUint::zero(), |value, (index, name)| { - let bit = var_map.get(name.as_str()).copied().ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "target circuit does not contain factor variable {name}" - )) - })?; - Ok::(if bit { + .fold(BigUint::zero(), |value, (index, name)| { + if var_map[name.as_str()] { value + (BigUint::one() << index) } else { value - }) + } }) }; - let left = decode(&self.p_vars)?; - let right = decode(&self.q_vars)?; + let left = decode(&self.p_vars); + let right = decode(&self.q_vars); if left <= right { (left, right) } else { diff --git a/src/rules/factoring_ilp.rs b/src/rules/factoring_ilp.rs index eef11666f..10c3006be 100644 --- a/src/rules/factoring_ilp.rs +++ b/src/rules/factoring_ilp.rs @@ -80,8 +80,6 @@ impl ReductionResult for ReductionFactoringToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Extract p bits (first factor) let p = (0..self.m) diff --git a/src/rules/feasibleregisterassignment_ilp.rs b/src/rules/feasibleregisterassignment_ilp.rs index 69b181c5d..f524541b1 100644 --- a/src/rules/feasibleregisterassignment_ilp.rs +++ b/src/rules/feasibleregisterassignment_ilp.rs @@ -33,8 +33,6 @@ impl ReductionResult for ReductionFeasibleRegisterAssignmentToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(&target_solution[..self.num_vertices]) } } @@ -147,7 +145,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/flowshopscheduling_ilp.rs b/src/rules/flowshopscheduling_ilp.rs index d5a4d60ee..266a05f02 100644 --- a/src/rules/flowshopscheduling_ilp.rs +++ b/src/rules/flowshopscheduling_ilp.rs @@ -39,8 +39,6 @@ impl ReductionResult for ReductionFSSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_jobs; let m = self.num_machines; @@ -201,7 +199,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/graph.rs b/src/rules/graph.rs index f89ffbb49..4a302bb4c 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -11,8 +11,8 @@ //! - JSON export for documentation and visualization use crate::rules::registry::{ - AggregateReduceFn, EdgeCapabilities, ParameterContractError, ReduceFn, ReductionEntry, - ReductionParameterContract, + AggregateReduceFn, EdgeCapabilities, ExecutedStep, ParameterContractError, ReduceFn, + ReductionEntry, ReductionParameterContract, }; use crate::rules::traits::{DynAggregateReductionResult, DynReductionResult}; use crate::types::ProblemParameters; @@ -22,7 +22,6 @@ use petgraph::visit::EdgeRef; use serde::Serialize; use std::any::Any; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; -use std::rc::Rc; type NodePathOrderKey<'a> = (usize, Vec<(&'static str, &'a BTreeMap)>); @@ -1528,21 +1527,51 @@ pub struct MatchedEntry { pub parameter_contract: Result, } +/// Apply already-constructed witness mappings in reverse order. +fn map_solution<'a>( + steps: impl DoubleEndedIterator, + target_solution: &dyn Any, +) -> crate::rules::ExtractionResult> { + let mut steps = steps.rev(); + let first = steps.next().expect("reduction path has no steps"); + let mut solution = first.extract_solution_dyn(target_solution)?; + for step in steps { + solution = step.extract_solution_dyn(solution.as_ref())?; + } + Ok(solution) +} + /// A composed reduction chain produced by [`ReductionGraph::reduce_along_path`]. /// /// Holds the intermediate reduction results from executing a multi-step /// reduction path. Provides access to the final target problem and /// solution extraction back to the source problem space. pub struct ReductionChain { - steps: Vec>, + pub(crate) steps: Vec, } impl ReductionChain { + pub(crate) fn execute( + source: &dyn Any, + reducers: &[ReduceFn], + ) -> Result { + let mut steps: Vec = Vec::with_capacity(reducers.len()); + for reduce in reducers { + let input = steps + .last() + .map(|step| step.witness.target_problem_any()) + .unwrap_or(source); + steps.push(reduce(input)?); + } + Ok(Self { steps }) + } + /// Get the final target problem as a type-erased reference. pub fn target_problem_any(&self) -> &dyn Any { self.steps .last() .expect("ReductionChain has no steps") + .witness .target_problem_any() } @@ -1560,12 +1589,10 @@ impl ReductionChain { &self, target_solution: &T, ) -> crate::rules::ExtractionResult { - let mut steps = self.steps.iter().rev(); - let first = steps.next().expect("ReductionChain has no steps"); - let mut solution = first.extract_solution_dyn(target_solution)?; - for step in steps { - solution = step.extract_solution_dyn(solution.as_ref())?; - } + let solution = map_solution( + self.steps.iter().map(|step| step.witness.as_ref()), + target_solution, + )?; solution .downcast::() .map(|solution| *solution) @@ -1578,11 +1605,14 @@ impl ReductionChain { target_solution: serde_json::Value, ) -> crate::rules::ExtractionResult { let last = self.steps.last().expect("ReductionChain has no steps"); - let mut solution = last.target_solution_from_json(target_solution)?; - for step in self.steps.iter().rev() { - solution = step.extract_solution_dyn(solution.as_ref())?; - } - self.steps[0].source_solution_json(solution.as_ref()) + let solution = last.witness.target_solution_from_json(target_solution)?; + let solution = map_solution( + self.steps.iter().map(|step| step.witness.as_ref()), + solution.as_ref(), + )?; + self.steps[0] + .witness + .source_solution_json(solution.as_ref()) } } @@ -1679,18 +1709,7 @@ impl ReductionGraph { }; edge_fns.push(reduce); } - // Execute the chain - let mut steps: Vec> = Vec::new(); - let step = (edge_fns[0])(source)?; - steps.push(step); - for edge_fn in &edge_fns[1..] { - let step = { - let prev_target = steps.last().unwrap().target_problem_any(); - edge_fn(prev_target)? - }; - steps.push(step); - } - Ok(Some(ReductionChain { steps })) + Ok(Some(ReductionChain::execute(source, &edge_fns)?)) } /// Execute an aggregate-value reduction path on a source problem instance. @@ -1744,7 +1763,7 @@ pub struct ExecutedPath { /// The variant-level path. pub path: ReductionPath, /// The executed reduction steps (one per hop), shared via `Rc`. - steps: Vec>, + steps: Vec, } impl ExecutedPath { @@ -1753,6 +1772,7 @@ impl ExecutedPath { self.steps .last() .expect("ExecutedPath has no steps") + .witness .target_problem_any() } @@ -1765,7 +1785,7 @@ impl ExecutedPath { ReductionGraph::compute_problem_parameters( &target.name, &target.variant, - result.target_problem_any(), + result.witness.target_problem_any(), ) }) .collect() @@ -1776,12 +1796,10 @@ impl ExecutedPath { &self, target_solution: &T, ) -> crate::rules::ExtractionResult { - let mut steps = self.steps.iter().rev(); - let first = steps.next().expect("ExecutedPath has no steps"); - let mut solution = first.extract_solution_dyn(target_solution)?; - for step in steps { - solution = step.extract_solution_dyn(solution.as_ref())?; - } + let solution = map_solution( + self.steps.iter().map(|step| step.witness.as_ref()), + target_solution, + )?; solution .downcast::() .map(|solution| *solution) @@ -1796,8 +1814,7 @@ impl ReductionGraph { paths: &[ReductionPath], source_instance: &dyn Any, ) -> Result, ExecutePathsError> { - let mut prefixes: HashMap, Vec>> = - HashMap::new(); + let mut prefixes: HashMap, ExecutedStep> = HashMap::new(); let mut executed = Vec::with_capacity(paths.len()); let mut batch_source: Option<&ReductionStep> = None; for (path_index, path) in paths.iter().enumerate() { @@ -1815,14 +1832,12 @@ impl ReductionGraph { } else { batch_source = Some(source); } - let source_prefix = vec![source.clone()]; - let mut chain = prefixes.get(&source_prefix).cloned().unwrap_or_default(); - prefixes.entry(source_prefix.clone()).or_default(); - let mut prefix = source_prefix; + let mut chain: Vec = Vec::with_capacity(path.len()); + let mut prefix = vec![source.clone()]; for pair in path.steps.windows(2) { prefix.push(pair[1].clone()); if let Some(cached) = prefixes.get(&prefix) { - chain = cached.clone(); + chain.push(cached.clone()); continue; } let source_node = self @@ -1857,12 +1872,12 @@ impl ReductionGraph { }; let current = chain .last() - .map(|step| step.target_problem_any()) + .map(|step| step.witness.target_problem_any()) .unwrap_or(source_instance); let result = reduce_fn(current) .map_err(|cause| ExecutePathsError::Reduction { path_index, cause })?; - chain.push(Rc::from(result)); - prefixes.insert(prefix.clone(), chain.clone()); + prefixes.insert(prefix.clone(), result.clone()); + chain.push(result); } executed.push(ExecutedPath { path: path.clone(), diff --git a/src/rules/graph_helpers.rs b/src/rules/graph_helpers.rs index 288c734c1..f9e74619e 100644 --- a/src/rules/graph_helpers.rs +++ b/src/rules/graph_helpers.rs @@ -2,82 +2,28 @@ use crate::topology::{Graph, SimpleGraph}; -/// Extract a Hamiltonian cycle vertex ordering from edge-selection configs on complete graphs. -/// -/// Given a graph and a binary `target_solution` over its edges (1 = selected), -/// walks the selected edges to produce a vertex permutation representing the cycle. -/// Returns an error if the selection does not form a valid Hamiltonian cycle. -pub(crate) fn edges_to_cycle_order( - graph: &G, - target_solution: &[bool], -) -> crate::rules::ExtractionResult> { +/// Order the vertices of a selected Hamiltonian cycle. +/// Target feasibility and the reduction's premises establish a single cycle. +pub(crate) fn edges_to_cycle_order(graph: &G, target_solution: &[bool]) -> Vec { let n = graph.num_vertices(); - if n == 0 { - return Ok(vec![]); - } - - let edges = graph.edges(); - if target_solution.len() != edges.len() { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} edge-selection values, got {}", - edges.len(), - target_solution.len() - ))); - } - let mut adjacency = vec![Vec::new(); n]; - let mut selected_count = 0usize; - for (idx, &selected) in target_solution.iter().enumerate() { - if !selected { - continue; + for ((u, v), &selected) in graph.edges().into_iter().zip(target_solution) { + if selected { + adjacency[u].push(v); + adjacency[v].push(u); } - let (u, v) = edges[idx]; - adjacency[u].push(v); - adjacency[v].push(u); - selected_count += 1; } - - if selected_count != n || adjacency.iter().any(|neighbors| neighbors.len() != 2) { - return Err(crate::rules::ExtractionError::invalid( - "selected edges do not form a Hamiltonian cycle", - )); - } - let mut order = Vec::with_capacity(n); - let mut visited = vec![false; n]; - let mut prev = None; - let mut current = 0usize; - + let mut previous = n; + let mut current = 0; for _ in 0..n { - if visited[current] { - return Err(crate::rules::ExtractionError::invalid( - "selected edges contain multiple disjoint cycles", - )); - } - visited[current] = true; order.push(current); let neighbors = &adjacency[current]; - let next = match prev { - Some(previous) => { - if neighbors[0] == previous { - neighbors[1] - } else { - neighbors[0] - } - } - None => neighbors[0], - }; - prev = Some(current); + let next = neighbors[usize::from(neighbors[0] == previous)]; + previous = current; current = next; } - - if current != 0 || visited.iter().any(|seen| !seen) { - return Err(crate::rules::ExtractionError::invalid( - "selected edges do not form one Hamiltonian cycle", - )); - } - - Ok(order) + order } /// Build the complement graph edges: edges between all non-adjacent vertex pairs. @@ -93,15 +39,3 @@ pub(crate) fn complement_edges(graph: &SimpleGraph) -> Vec<(usize, usize)> { } edges } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn rejects_disjoint_selected_cycles() { - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5)]); - - assert!(edges_to_cycle_order(&graph, &[true; 6]).is_err()); - } -} diff --git a/src/rules/graphpartitioning_ilp.rs b/src/rules/graphpartitioning_ilp.rs index 3186696a2..5aeab2756 100644 --- a/src/rules/graphpartitioning_ilp.rs +++ b/src/rules/graphpartitioning_ilp.rs @@ -34,8 +34,6 @@ impl ReductionResult for ReductionGraphPartitioningToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) @@ -93,20 +91,23 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/graphpartitioning_maxcut.rs b/src/rules/graphpartitioning_maxcut.rs index 658bfe136..53bc7bad7 100644 --- a/src/rules/graphpartitioning_maxcut.rs +++ b/src/rules/graphpartitioning_maxcut.rs @@ -26,28 +26,29 @@ impl ReductionResult for ReductionGPToMaxCut { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } #[cfg(any(test, feature = "example-db"))] fn issue_example() -> GraphPartitioning { - GraphPartitioning::new(SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (1, 2), - (1, 3), - (2, 3), - (2, 4), - (3, 4), - (3, 5), - (4, 5), - ], - )) + GraphPartitioning::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (2, 3), + (2, 4), + (3, 4), + (3, 5), + (4, 5), + ], + ) + .unwrap(), + ) } fn complete_graph_edges_and_weights(graph: &SimpleGraph) -> (Vec<(usize, usize)>, Vec) { @@ -84,7 +85,12 @@ impl ReduceTo> for GraphPartitioning { fn reduce_to(&self) -> Result { let (edges, weights) = complete_graph_edges_and_weights(self.graph()); - let target = MaxCut::new(SimpleGraph::new(self.num_vertices(), edges), weights); + let target = MaxCut::new( + SimpleGraph::new(self.num_vertices(), edges) + .map_err(>>::target_construction)?, + weights, + ) + .map_err(>>::target_construction)?; Ok(ReductionGPToMaxCut { target }) } diff --git a/src/rules/graphpartitioning_qubo.rs b/src/rules/graphpartitioning_qubo.rs index 9840bc491..b4f33077d 100644 --- a/src/rules/graphpartitioning_qubo.rs +++ b/src/rules/graphpartitioning_qubo.rs @@ -28,8 +28,6 @@ impl ReductionResult for ReductionGraphPartitioningToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -52,7 +50,7 @@ impl ReduceTo> for GraphPartitioning { let penalty = edge_count .checked_add(1) .ok_or_else(|| overflow("computing the balance penalty"))?; - let mut matrix = vec![vec![0i64; n]; n]; + let mut matrix = vec![std::collections::BTreeMap::new(); n]; let mut degrees = vec![0usize; n]; let edges = self.graph().edges(); @@ -70,11 +68,11 @@ impl ReduceTo> for GraphPartitioning { .ok_or_else(|| overflow("computing a balance coefficient"))?, ) .ok_or_else(|| overflow("computing a balance coefficient"))?; - row[i] = degree + *row.entry(i).or_insert(0i64) = degree .checked_add(balance_linear) .ok_or_else(|| overflow("combining QUBO diagonal coefficients"))?; - for value in row.iter_mut().skip(i + 1) { - *value = penalty + for j in (i + 1)..n { + *row.entry(j).or_insert(0i64) = penalty .checked_mul(2) .ok_or_else(|| overflow("computing a balance interaction coefficient"))?; } @@ -82,13 +80,14 @@ impl ReduceTo> for GraphPartitioning { for (u, v) in edges { let (lo, hi) = if u < v { (u, v) } else { (v, u) }; - matrix[lo][hi] = matrix[lo][hi] + let coefficient = matrix[lo].entry(hi).or_insert(0i64); + *coefficient = coefficient .checked_sub(2) .ok_or_else(|| overflow("adding a cut interaction coefficient"))?; } Ok(ReductionGraphPartitioningToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { + target: QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::< GraphPartitioning, QUBO, @@ -106,20 +105,23 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( - GraphPartitioning::new(SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (1, 2), - (1, 3), - (2, 3), - (2, 4), - (3, 4), - (3, 5), - (4, 5), - ], - )), + GraphPartitioning::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (2, 3), + (2, 4), + (3, 4), + (3, 5), + (4, 5), + ], + ) + .unwrap(), + ), SolutionPair { source_config: serde_json::json!(vec![false, false, false, true, true, true]), target_config: serde_json::json!(vec![false, false, false, true, true, true]), diff --git a/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs b/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs index 07576fe0e..ff91a4b8c 100644 --- a/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs +++ b/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs @@ -51,22 +51,8 @@ impl ReductionResult for ReductionHamiltonianCircuitToBiconnectivityAugmentation &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - if !crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)? - .0 - { - return Err(crate::rules::ExtractionError::invalid( - "target augmentation is infeasible", - )); - } - Ok({ let n = self.num_vertices; - if n < 3 { - return Err(crate::rules::ExtractionError::invalid( - "a Hamiltonian circuit requires at least three vertices", - )); - } - // Collect selected edges (those with config value 1) let mut adj: Vec> = vec![vec![]; n]; for (i, &(u, v)) in self.potential_edges.iter().enumerate() { @@ -76,43 +62,17 @@ impl ReductionResult for ReductionHamiltonianCircuitToBiconnectivityAugmentation } } - // Check that every vertex has exactly degree 2 (Hamiltonian cycle) - if adj.iter().any(|neighbors| neighbors.len() != 2) { - return Err(crate::rules::ExtractionError::invalid( - "selected edges do not give every source vertex degree two", - )); - } - - // Walk the cycle starting from vertex 0 let mut circuit = Vec::with_capacity(n); - circuit.push(0); - let mut prev = 0; - let mut current = adj[0][0]; - while current != 0 { + let mut previous = n; + let mut current = 0; + for _ in 0..n { circuit.push(current); - let next = if adj[current][0] == prev { - adj[current][1] - } else { - adj[current][0] - }; - prev = current; + let neighbors = &adj[current]; + let next = neighbors[usize::from(neighbors[0] == previous)]; + previous = current; current = next; - - // Safety: if we've visited more than n vertices, something is wrong - if circuit.len() > n { - return Err(crate::rules::ExtractionError::invalid( - "selected edges revisit a source vertex", - )); - } - } - - if circuit.len() == n { - circuit - } else { - return Err(crate::rules::ExtractionError::invalid( - "selected edges do not form a spanning circuit", - )); } + circuit }) } } @@ -131,7 +91,7 @@ impl ReduceTo> for HamiltonianCircu let n = self.num_vertices(); if n < 3 { return Ok(ReductionHamiltonianCircuitToBiconnectivityAugmentation { - target: BiconnectivityAugmentation::new(SimpleGraph::empty(3), vec![], 0), + target: BiconnectivityAugmentation::new(SimpleGraph::empty(3), vec![], 0).map_err(>>::target_construction)?, num_vertices: n, potential_edges: vec![], }); @@ -160,7 +120,10 @@ impl ReduceTo> for HamiltonianCircu >("converting the vertex count to the target budget") })?; - let target = BiconnectivityAugmentation::new(initial_graph, potential_weights, budget); + let target = BiconnectivityAugmentation::new(initial_graph, potential_weights, budget) + .map_err( + >>::target_construction, + )?; Ok(ReductionHamiltonianCircuitToBiconnectivityAugmentation { target, diff --git a/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs b/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs index abe6c3783..4ae3e6128 100644 --- a/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs +++ b/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs @@ -27,9 +27,10 @@ impl ReductionResult for ReductionHamiltonianCircuitToBottleneckTravelingSalesma &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::graph_helpers::edges_to_cycle_order(self.target.graph(), target_solution) + Ok(crate::rules::graph_helpers::edges_to_cycle_order( + self.target.graph(), + target_solution, + )) } } @@ -50,7 +51,8 @@ impl ReduceTo for HamiltonianCircuit { .into_iter() .map(|(u, v)| if self.graph().has_edge(u, v) { 1 } else { 2 }) .collect(); - let target = BottleneckTravelingSalesman::new(target_graph, weights); + let target = BottleneckTravelingSalesman::new(target_graph, weights) + .map_err(>::target_construction)?; Ok(ReductionHamiltonianCircuitToBottleneckTravelingSalesman { target }) } diff --git a/src/rules/hamiltoniancircuit_hamiltonianpath.rs b/src/rules/hamiltoniancircuit_hamiltonianpath.rs index 4d15d9227..7e6589b56 100644 --- a/src/rules/hamiltoniancircuit_hamiltonianpath.rs +++ b/src/rules/hamiltoniancircuit_hamiltonianpath.rs @@ -40,40 +40,25 @@ impl ReductionResult for ReductionHamiltonianCircuitToHamiltonianPath { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_original_vertices; if n == 0 { return Ok(vec![]); } - let v_prime = n; // index of duplicated vertex v' - let s = n + 1; // pendant attached to v=0 - let t = n + 2; // pendant attached to v' - - // The two pendants force any valid witness to have endpoints s and t. - let reversed; - let oriented = match (target_solution.first(), target_solution.last()) { - (Some(&start), Some(&end)) if start == s && end == t => target_solution, - (Some(&start), Some(&end)) if start == t && end == s => { - reversed = target_solution.iter().copied().rev().collect::>(); - reversed.as_slice() - } - _ => { - return Err(crate::rules::ExtractionError::invalid( - "target path does not have the required pendant endpoints", - )) - } - }; - - if oriented.get(1) != Some(&0) || oriented.get(n + 1) != Some(&v_prime) { - return Err(crate::rules::ExtractionError::invalid( - "target path does not traverse the duplicated source vertex correctly", - )); + let s = n + 1; + // Pendant vertices force the path's endpoints; orient from s. + if target_solution[0] == s { + target_solution[1..=n].to_vec() + } else { + target_solution + .iter() + .rev() + .skip(1) + .take(n) + .copied() + .collect() } - - oriented[1..=n].to_vec() }) } } @@ -129,7 +114,8 @@ impl ReduceTo> for HamiltonianCircuit // 4. Add pendant edge {t, v'} edges.push((t, v_prime)); - let target_graph = SimpleGraph::new(n + 3, edges); + let target_graph = SimpleGraph::new(n + 3, edges) + .map_err(>>::target_construction)?; let target = HamiltonianPath::new(target_graph); Ok(ReductionHamiltonianCircuitToHamiltonianPath { diff --git a/src/rules/hamiltoniancircuit_longestcircuit.rs b/src/rules/hamiltoniancircuit_longestcircuit.rs index 34ae58de9..2bb114810 100644 --- a/src/rules/hamiltoniancircuit_longestcircuit.rs +++ b/src/rules/hamiltoniancircuit_longestcircuit.rs @@ -27,15 +27,10 @@ impl ReductionResult for ReductionHamiltonianCircuitToLongestCircuit { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target circuit does not certify a Hamiltonian circuit", - )); - } - - crate::rules::graph_helpers::edges_to_cycle_order(self.target.graph(), target_solution) + Ok(crate::rules::graph_helpers::edges_to_cycle_order( + self.target.graph(), + target_solution, + )) } } @@ -69,7 +64,13 @@ impl ReduceTo> for HamiltonianCircuit Result { let n = self.num_vertices(); let edges = self.graph().edges(); - let target = LongestCircuit::new(SimpleGraph::new(n, edges), vec![1i64; self.num_edges()]); + let target = LongestCircuit::new( + SimpleGraph::new(n, edges).map_err( + >>::target_construction, + )?, + vec![1i64; self.num_edges()], + ) + .map_err(>>::target_construction)?; Ok(ReductionHamiltonianCircuitToLongestCircuit { target }) } } diff --git a/src/rules/hamiltoniancircuit_quadraticassignment.rs b/src/rules/hamiltoniancircuit_quadraticassignment.rs index 4cd1e5264..66913270e 100644 --- a/src/rules/hamiltoniancircuit_quadraticassignment.rs +++ b/src/rules/hamiltoniancircuit_quadraticassignment.rs @@ -29,14 +29,6 @@ impl ReductionResult for ReductionHamiltonianCircuitToQuadraticAssignment { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target assignment does not certify a Hamiltonian circuit", - )); - } - // Zero cost makes this permutation itself a Hamiltonian circuit. Ok(target_solution.to_vec()) } @@ -81,7 +73,8 @@ impl ReduceTo for HamiltonianCircuit { }) .collect(); - let target = QuadraticAssignment::new(cost_matrix, distance_matrix); + let target = QuadraticAssignment::new(cost_matrix, distance_matrix) + .map_err(>::target_construction)?; Ok(ReductionHamiltonianCircuitToQuadraticAssignment { target }) } } diff --git a/src/rules/hamiltoniancircuit_ruralpostman.rs b/src/rules/hamiltoniancircuit_ruralpostman.rs index e1e2d321d..43c4dcc5d 100644 --- a/src/rules/hamiltoniancircuit_ruralpostman.rs +++ b/src/rules/hamiltoniancircuit_ruralpostman.rs @@ -50,8 +50,6 @@ impl ReductionResult for ReductionHamiltonianCircuitToRuralPostman { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // The target solution is edge multiplicities. // Required edges are indices 0..n (the {v_i^a, v_i^b} edges). @@ -90,11 +88,6 @@ impl ReductionResult for ReductionHamiltonianCircuitToRuralPostman { for _ in 0..n { cycle.push(current); let next = successor[current]; - if next == usize::MAX { - return Err(crate::rules::ExtractionError::invalid( - "target tour does not provide one successor for every source vertex", - )); - } current = next; } @@ -103,7 +96,21 @@ impl ReductionResult for ReductionHamiltonianCircuitToRuralPostman { } } +impl crate::rules::AggregateReductionResult for ReductionHamiltonianCircuitToRuralPostman { + type Source = HamiltonianCircuit; + type Target = RuralPostman; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { + crate::types::Or(self.n >= 3 && value.0 == Some(2 * self.n as i64)) + } +} + #[reduction( + aggregate = custom, transform = exact { num_vertices = "2 * num_vertices", num_edges = "num_vertices + 2 * num_edges", @@ -141,8 +148,10 @@ impl ReduceTo> for HamiltonianCircuit>>::target_construction)?; + let target = RuralPostman::new(target_graph, edge_weights, required_edges) + .map_err(>>::target_construction)?; Ok(ReductionHamiltonianCircuitToRuralPostman { target, @@ -160,7 +169,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target tour does not certify a Hamiltonian circuit", - )); - } // Service arc i corresponds to source vertex i. Ok(target_solution.to_vec()) } diff --git a/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs b/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs index e2592e77e..9fac217bc 100644 --- a/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs +++ b/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs @@ -31,14 +31,8 @@ impl ReductionResult for ReductionHamiltonianCircuitToStrongConnectivityAugmenta &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.n; - if n == 0 { - return Ok(vec![]); - } - // Build directed adjacency from selected arcs. let candidate_arcs = self.target.candidate_arcs(); let mut successors = vec![Vec::new(); n]; @@ -52,20 +46,8 @@ impl ReductionResult for ReductionHamiltonianCircuitToStrongConnectivityAugmenta // Walk the directed cycle starting from vertex 0. let mut order = Vec::with_capacity(n); let mut current = 0; - let mut visited = vec![false; n]; for _ in 0..n { - if visited[current] { - return Err(crate::rules::ExtractionError::invalid( - "selected arcs revisit a source vertex", - )); - } - visited[current] = true; order.push(current); - if successors[current].len() != 1 { - return Err(crate::rules::ExtractionError::invalid( - "selected arcs do not provide one successor for every source vertex", - )); - } current = successors[current][0]; } diff --git a/src/rules/hamiltoniancircuit_travelingsalesman.rs b/src/rules/hamiltoniancircuit_travelingsalesman.rs index 8e90e4ac8..52eff7dad 100644 --- a/src/rules/hamiltoniancircuit_travelingsalesman.rs +++ b/src/rules/hamiltoniancircuit_travelingsalesman.rs @@ -27,9 +27,10 @@ impl ReductionResult for ReductionHamiltonianCircuitToTravelingSalesman { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::graph_helpers::edges_to_cycle_order(self.target.graph(), target_solution) + Ok(crate::rules::graph_helpers::edges_to_cycle_order( + self.target.graph(), + target_solution, + )) } } @@ -50,7 +51,9 @@ impl ReduceTo> for HamiltonianCircuit>>::target_construction, + )?; Ok(ReductionHamiltonianCircuitToTravelingSalesman { target }) } diff --git a/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs b/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs index c783517cb..347c854ec 100644 --- a/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs +++ b/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs @@ -25,9 +25,10 @@ impl ReductionResult for ReductionHamiltonianPathToDegreeConstrainedSpanningTree &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - extract_hamiltonian_order(self.target.graph(), target_solution) + Ok(extract_hamiltonian_order( + self.target.graph(), + target_solution, + )) } } @@ -42,20 +43,22 @@ impl ReduceTo> for HamiltonianPath Result { let target = DegreeConstrainedSpanningTree::new( - SimpleGraph::new(self.graph().num_vertices(), self.graph().edges()), + SimpleGraph::new(self.graph().num_vertices(), self.graph().edges()).map_err( + >>::target_construction, + )?, 2, - ); + ) + .map_err( + >>::target_construction, + )?; Ok(ReductionHamiltonianPathToDegreeConstrainedSpanningTree { target }) } } -fn extract_hamiltonian_order( - graph: &SimpleGraph, - target_solution: &[bool], -) -> crate::rules::ExtractionResult> { +fn extract_hamiltonian_order(graph: &SimpleGraph, target_solution: &[bool]) -> Vec { let num_vertices = graph.num_vertices(); if num_vertices < 2 { - return Ok((0..num_vertices).collect()); + return (0..num_vertices).collect(); } let edges = graph.edges(); @@ -74,46 +77,24 @@ fn extract_hamiltonian_order( .filter_map(|(vertex, neighbors)| (neighbors.len() == 1).then_some(vertex)) .collect(); endpoints.sort_unstable(); - if endpoints.len() != 2 { - return Err(crate::rules::ExtractionError::invalid( - "selected edges do not form a Hamiltonian path", - )); - } - let mut order = Vec::with_capacity(num_vertices); - let mut visited = vec![false; num_vertices]; let mut previous = None; let mut current = endpoints[0]; - loop { - if visited[current] { - return Err(crate::rules::ExtractionError::invalid( - "selected edges contain a cycle", - )); - } - visited[current] = true; order.push(current); - - let next = adjacency[current] + match adjacency[current] .iter() .copied() - .find(|&neighbor| Some(neighbor) != previous && !visited[neighbor]); - match next { - Some(next_vertex) => { + .find(|&neighbor| Some(neighbor) != previous) + { + Some(next) => { previous = Some(current); - current = next_vertex; + current = next; } None => break, } } - - if order.len() == num_vertices { - Ok(order) - } else { - Err(crate::rules::ExtractionError::invalid( - "selected edges do not span every source vertex", - )) - } + order } #[cfg(feature = "example-db")] @@ -136,19 +117,22 @@ fn edge_config_for_path(graph: &SimpleGraph, path: &[usize]) -> Vec { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { fn source_example() -> HamiltonianPath { - HamiltonianPath::new(SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (1, 3), - (2, 3), - (3, 4), - (3, 5), - (4, 2), - (5, 1), - ], - )) + HamiltonianPath::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 3), + (2, 3), + (3, 4), + (3, 5), + (4, 2), + (5, 1), + ], + ) + .unwrap(), + ) } vec![crate::example_db::specs::RuleExampleSpec { diff --git a/src/rules/hamiltonianpath_ilp.rs b/src/rules/hamiltonianpath_ilp.rs index 378545040..ef6d56676 100644 --- a/src/rules/hamiltonianpath_ilp.rs +++ b/src/rules/hamiltonianpath_ilp.rs @@ -39,9 +39,12 @@ impl ReductionResult for ReductionHamiltonianPathToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode(target_solution, self.num_vertices, self.num_vertices, 0) + Ok(one_hot_decode( + target_solution, + self.num_vertices, + self.num_vertices, + 0, + )) } } @@ -122,7 +125,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/hamiltonianpath_isomorphicspanningtree.rs b/src/rules/hamiltonianpath_isomorphicspanningtree.rs index b95b555d4..dd7802124 100644 --- a/src/rules/hamiltonianpath_isomorphicspanningtree.rs +++ b/src/rules/hamiltonianpath_isomorphicspanningtree.rs @@ -32,8 +32,6 @@ impl ReductionResult for ReductionHPToIST { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs b/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs index 0a9186ee1..28baec3a0 100644 --- a/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs +++ b/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs @@ -33,14 +33,6 @@ impl ReductionResult for ReductionHPBTVToLP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target path does not certify a Hamiltonian source-target path", - )); - } - let mut adjacency = vec![Vec::new(); self.target.num_vertices()]; for (&selected, (u, v)) in target_solution.iter().zip(self.target.graph().edges()) { if selected { @@ -105,7 +97,8 @@ impl ReduceTo> for HamiltonianPathBetweenTwoVertic edge_lengths, self.source_vertex(), self.target_vertex(), - ); + ) + .map_err(>>::target_construction)?; Ok(ReductionHPBTVToLP { target }) } @@ -120,10 +113,11 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/highlyconnecteddeletion_ilp.rs b/src/rules/highlyconnecteddeletion_ilp.rs index 1fb2c0b47..a980e4723 100644 --- a/src/rules/highlyconnecteddeletion_ilp.rs +++ b/src/rules/highlyconnecteddeletion_ilp.rs @@ -64,32 +64,15 @@ impl ReductionResult for ReductionHighlyConnectedDeletionToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let mut cluster_of: Vec> = vec![None; vertex_count(&self.clusters)]; for (c, cluster) in self.clusters.iter().enumerate() { if target_solution[c] == 1 { for &v in cluster { - if cluster_of[v].is_some() { - return Err(crate::rules::ExtractionError::invalid(format!( - "vertex {v} belongs to multiple selected clusters" - ))); - } cluster_of[v] = Some(c); } - } else if target_solution[c] != 0 { - return Err(crate::rules::ExtractionError::invalid(format!( - "cluster selection {c} is not binary" - ))); } } - if let Some(vertex) = cluster_of.iter().position(Option::is_none) { - return Err(crate::rules::ExtractionError::invalid(format!( - "vertex {vertex} has no selected cluster" - ))); - } - Ok(self .edges .iter() @@ -117,13 +100,16 @@ fn vertex_count(clusters: &[Vec]) -> usize { /// Order: all `n` singletons first (subset ids `1, 2, 4, ...`), then larger /// feasible clusters listed by ascending bitmask of their vertex set. This /// gives a stable variable layout; tests pin the singleton prefix. -fn enumerate_feasible_clusters(graph: &SimpleGraph) -> Vec> { +fn enumerate_feasible_clusters( + graph: &SimpleGraph, +) -> Result>, crate::rules::ReductionError> { let n = graph.num_vertices(); - debug_assert!( - n < 64, - "enumerate_feasible_clusters requires n < 64 due to u64 subset mask; got n={}", - n - ); + if n >= u64::BITS as usize { + return Err(crate::rules::ReductionError::integer_overflow::< + HighlyConnectedDeletion, + ILP, + >("enumerating vertex subsets with a u64 mask")); + } let mut clusters: Vec> = Vec::new(); // Singletons first. @@ -132,7 +118,7 @@ fn enumerate_feasible_clusters(graph: &SimpleGraph) -> Vec> { } if n < 3 { - return clusters; + return Ok(clusters); } // Larger feasible clusters by ascending subset bitmask. @@ -147,7 +133,7 @@ fn enumerate_feasible_clusters(graph: &SimpleGraph) -> Vec> { } } - clusters + Ok(clusters) } #[reduction( @@ -165,7 +151,7 @@ impl ReduceTo> for HighlyConnectedDeletion { fn reduce_to(&self) -> Result { let graph = self.graph(); let n = graph.num_vertices(); - let clusters = enumerate_feasible_clusters(graph); + let clusters = enumerate_feasible_clusters(graph)?; let num_vars = clusters.len(); // Partition constraints: for every vertex v, sum_{S : v in S} x_S = 1. @@ -223,10 +209,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/ilp_bool_ilp_i64.rs b/src/rules/ilp_bool_ilp_i64.rs index c5bb1df86..bb2a8f9fa 100644 --- a/src/rules/ilp_bool_ilp_i64.rs +++ b/src/rules/ilp_bool_ilp_i64.rs @@ -25,8 +25,6 @@ impl ReductionResult for ReductionBinaryILPToIntILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/ilp_casts.rs b/src/rules/ilp_casts.rs deleted file mode 100644 index 586fe6605..000000000 --- a/src/rules/ilp_casts.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! Numeric variant reductions for ILP. - -use crate::models::algebraic::{Comparison, LinearConstraint, VariableDomain, ILP}; -use crate::reduction; -use crate::rules::{ReduceTo, ReductionError, ReductionResult}; -use crate::types::i64_to_exact_f64; - -#[derive(Debug, Clone)] -pub struct ReductionILPToFloat { - source: ILP, - target: ILP, -} - -impl ReductionILPToFloat { - fn new(source: &ILP) -> Result { - let convert = |coefficient: i64| { - i64_to_exact_f64(coefficient) - .map_err(ReductionError::inexact_float_conversion::, ILP>) - }; - let constraints = source - .constraints() - .iter() - .map(|constraint| { - let terms = constraint - .terms() - .iter() - .map(|&(variable, coefficient)| Ok((variable, convert(coefficient)?))) - .collect::, ReductionError>>()?; - let rhs = convert(constraint.rhs())?; - Ok(match constraint.comparison() { - Comparison::Le => LinearConstraint::le(terms, rhs), - Comparison::Ge => LinearConstraint::ge(terms, rhs), - Comparison::Eq => LinearConstraint::eq(terms, rhs), - }) - }) - .collect::, ReductionError>>()?; - let objective = source - .objective() - .iter() - .map(|&(variable, coefficient)| Ok((variable, convert(coefficient)?))) - .collect::, ReductionError>>()?; - let target = ILP::with_variables( - source.variables().to_vec(), - constraints, - objective, - source.sense(), - ) - .map_err(ReductionError::construction::, ILP>)?; - Ok(Self { - source: source.clone(), - target, - }) - } -} - -impl ReductionResult for ReductionILPToFloat { - type Source = ILP; - type Target = ILP; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_solution( - &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !self.source.is_feasible(target_solution)? { - return Err(crate::rules::ExtractionError::invalid( - "the floating-point assignment violates the source integer ILP", - )); - } - Ok(target_solution.clone()) - } -} - -#[reduction( - transform = exact { - num_vars = "num_vars", - num_constraints = "num_constraints", - num_nonzeros = "num_nonzeros", - }, -)] -impl ReduceTo> for ILP { - type Result = ReductionILPToFloat; - - fn reduce_to(&self) -> Result { - ReductionILPToFloat::new(self) - } -} - -#[reduction( - transform = exact { - num_vars = "num_vars", - num_constraints = "num_constraints", - num_nonzeros = "num_nonzeros", - }, -)] -impl ReduceTo> for ILP { - type Result = ReductionILPToFloat; - - fn reduce_to(&self) -> Result { - ReductionILPToFloat::new(self) - } -} - -#[cfg(test)] -#[path = "../unit_tests/rules/ilp_casts.rs"] -mod tests; diff --git a/src/rules/ilp_helpers.rs b/src/rules/ilp_helpers.rs index 4f982bdc5..48d8dd728 100644 --- a/src/rules/ilp_helpers.rs +++ b/src/rules/ilp_helpers.rs @@ -43,62 +43,34 @@ pub fn mccormick_product>( ] } -/// Decode one selected item from each slot of a column-major one-hot matrix. +/// Decode a column-major assignment whose constraints select one item per slot. pub fn one_hot_decode( solution: &[i64], num_items: usize, num_slots: usize, var_offset: usize, -) -> crate::rules::ExtractionResult> { - let assignment: Vec = (0..num_slots) +) -> Vec { + (0..num_slots) .map(|slot| { - let mut selected = - (0..num_items).filter(|&item| solution[var_offset + item * num_slots + slot] == 1); - let item = selected.next().ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "assignment slot {slot} has no selected item" - )) - })?; - if selected.next().is_some() { - return Err(crate::rules::ExtractionError::invalid(format!( - "assignment slot {slot} has multiple selected items" - ))); - } - Ok(item) + (0..num_items) + .filter(|&item| solution[var_offset + item * num_slots + slot] == 1) + .sum() }) - .collect::>()?; - - let mut assigned = vec![false; num_items]; - for &item in &assignment { - if std::mem::replace(&mut assigned[item], true) { - return Err(crate::rules::ExtractionError::invalid(format!( - "item {item} is selected for multiple assignment slots" - ))); - } - } - Ok(assignment) + .collect() } -/// Decode one selected column from each row of a row-major one-hot matrix. +/// Decode a row-major assignment whose constraints select one column per row. pub fn one_hot_decode_rows( solution: &[i64], num_rows: usize, num_columns: usize, var_offset: usize, -) -> crate::rules::ExtractionResult> { +) -> Vec { (0..num_rows) .map(|row| { - let mut selected = (0..num_columns) - .filter(|&column| solution[var_offset + row * num_columns + column] == 1); - match (selected.next(), selected.next()) { - (Some(column), None) => Ok(column), - (None, _) => Err(crate::rules::ExtractionError::invalid(format!( - "assignment row {row} has no selected column" - ))), - (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( - "assignment row {row} has multiple selected columns" - ))), - } + (0..num_columns) + .filter(|&column| solution[var_offset + row * num_columns + column] == 1) + .sum() }) .collect() } diff --git a/src/rules/ilp_i64_ilp_bool.rs b/src/rules/ilp_i64_ilp_bool.rs index ea783e12f..2d53911cf 100644 --- a/src/rules/ilp_i64_ilp_bool.rs +++ b/src/rules/ilp_i64_ilp_bool.rs @@ -84,29 +84,20 @@ impl ReductionResult for ReductionIntILPToBinaryILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - self.encodings + Ok(self + .encodings .iter() .map(|encoding| { - encoding.weights.iter().enumerate().try_fold( - encoding.lower_bound, - |value, (offset, &weight)| { - let term = weight - .checked_mul(target_solution[encoding.start + offset]) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "binary ILP decoding multiplication overflowed i64", - ) - })?; - value.checked_add(term).ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "binary ILP decoding sum overflowed i64", - ) - }) - }, - ) + let offset: i64 = encoding + .weights + .iter() + .enumerate() + .filter(|(offset, _)| target_solution[encoding.start + offset] == 1) + .map(|(_, &weight)| weight) + .sum(); + encoding.lower_bound + offset }) - .collect() + .collect()) } } diff --git a/src/rules/ilp_i64_ilp_f64.rs b/src/rules/ilp_i64_ilp_f64.rs new file mode 100644 index 000000000..8098a115d --- /dev/null +++ b/src/rules/ilp_i64_ilp_f64.rs @@ -0,0 +1,86 @@ +//! Exact integer-to-floating coefficient reductions for ILP. +//! +//! Preserve variable domains and every formal linear expression by converting +//! coefficients and right-hand sides exactly within the supported numeric range. +//! Solution extraction is the identity map. Numerical backend capabilities are +//! independent of this reduction. + +use crate::models::algebraic::{Comparison, LinearConstraint, VariableDomain, ILP}; +use crate::reduction; +use crate::rules::{ReduceTo, ReductionError, VariantReductionResult}; +use crate::types::i64_to_exact_f64; + +pub type ReductionILPToFloat = VariantReductionResult, ILP>; + +fn reduce_coefficients( + source: &ILP, +) -> Result, ReductionError> { + let convert = |coefficient: i64| { + i64_to_exact_f64(coefficient) + .map_err(ReductionError::inexact_float_conversion::, ILP>) + }; + let constraints = source + .constraints() + .iter() + .map(|constraint| { + let terms = constraint + .terms() + .iter() + .map(|&(variable, coefficient)| Ok((variable, convert(coefficient)?))) + .collect::, ReductionError>>()?; + let rhs = convert(constraint.rhs())?; + Ok(match constraint.comparison() { + Comparison::Le => LinearConstraint::le(terms, rhs), + Comparison::Ge => LinearConstraint::ge(terms, rhs), + Comparison::Eq => LinearConstraint::eq(terms, rhs), + }) + }) + .collect::, ReductionError>>()?; + let objective = source + .objective() + .iter() + .map(|&(variable, coefficient)| Ok((variable, convert(coefficient)?))) + .collect::, ReductionError>>()?; + let target = ILP::with_variables( + source.variables().to_vec(), + constraints, + objective, + source.sense(), + ) + .map_err(ReductionError::construction::, ILP>)?; + Ok(VariantReductionResult::new(target)) +} + +#[reduction( + transform = exact { + num_vars = "num_vars", + num_constraints = "num_constraints", + num_nonzeros = "num_nonzeros", + }, +)] +impl ReduceTo> for ILP { + type Result = ReductionILPToFloat; + + fn reduce_to(&self) -> Result { + reduce_coefficients(self) + } +} + +#[reduction( + transform = exact { + num_vars = "num_vars", + num_constraints = "num_constraints", + num_nonzeros = "num_nonzeros", + }, +)] +impl ReduceTo> for ILP { + type Result = ReductionILPToFloat; + + fn reduce_to(&self) -> Result { + reduce_coefficients(self) + } +} + +#[cfg(test)] +#[path = "../unit_tests/rules/ilp_i64_ilp_f64.rs"] +mod tests; diff --git a/src/rules/ilp_qubo.rs b/src/rules/ilp_qubo.rs index fbe885057..f6dd95dcd 100644 --- a/src/rules/ilp_qubo.rs +++ b/src/rules/ilp_qubo.rs @@ -39,14 +39,6 @@ impl ReductionResult for ReductionILPToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).is_valid() { - return Err(crate::rules::ExtractionError::invalid( - "target QUBO configuration does not certify a feasible ILP assignment", - )); - } - Ok(target_solution[..self.num_original_vars] .iter() .map(|&value| i64::from(value)) @@ -252,7 +244,7 @@ impl ReduceTo> for ILP { feasible_energy_range(&c_vec, &b_vec, penalty)?; // QUBO = -diag(c + 2·P·b·A) + P·A^T·A - let mut matrix = vec![vec![0_i64; nq]; nq]; + let mut matrix = vec![std::collections::BTreeMap::new(); nq]; // Compute b·A (b_vec dot each column of a_ext) let mut ba = vec![0_i64; nq]; @@ -281,14 +273,17 @@ impl ReduceTo> for ILP { "computing a QUBO diagonal penalty", ) })?; - matrix[j][j] = c_vec[j] - .checked_add(penalty_term) - .and_then(i64::checked_neg) - .ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::, QUBO>( - "computing a QUBO diagonal coefficient", - ) - })?; + matrix[j].insert( + j, + c_vec[j] + .checked_add(penalty_term) + .and_then(i64::checked_neg) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::, QUBO>( + "computing a QUBO diagonal coefficient", + ) + })?, + ); } // A^T·A contribution (upper-triangular convention) @@ -308,13 +303,17 @@ impl ReduceTo> for ILP { "computing a quadratic QUBO diagonal penalty", ) })?; - row_i[i] = row_i[i].checked_add(diagonal).ok_or_else(|| { + let coefficient = row_i.entry(i).or_insert(0i64); + *coefficient = coefficient.checked_add(diagonal).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::, QUBO>( "adding a quadratic QUBO diagonal penalty", ) })?; // Off-diagonal for j in (i + 1)..nq { + if row[j] == 0 { + continue; + } let interaction = penalty .checked_mul(row[i]) .and_then(|value| value.checked_mul(row[j])) @@ -324,7 +323,8 @@ impl ReduceTo> for ILP { "computing a quadratic QUBO interaction penalty", ) })?; - row_i[j] = row_i[j].checked_add(interaction).ok_or_else(|| { + let coefficient = row_i.entry(j).or_insert(0i64); + *coefficient = coefficient.checked_add(interaction).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::, QUBO>( "adding a quadratic QUBO interaction penalty", ) @@ -334,7 +334,7 @@ impl ReduceTo> for ILP { } Ok(ReductionILPToQUBO { - target: QUBO::from_matrix(matrix) + target: QUBO::from_rows(matrix) .map_err(crate::rules::ReductionError::construction::, QUBO>)?, num_original_vars: n, sense: self.sense(), diff --git a/src/rules/integerknapsack_ilp.rs b/src/rules/integerknapsack_ilp.rs index 8eb5b2aca..e74dddbdc 100644 --- a/src/rules/integerknapsack_ilp.rs +++ b/src/rules/integerknapsack_ilp.rs @@ -26,8 +26,6 @@ impl ReductionResult for ReductionIntegerKnapsackToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(target_solution) } } diff --git a/src/rules/integralflowbundles_ilp.rs b/src/rules/integralflowbundles_ilp.rs index d4220c41e..5399b9a6e 100644 --- a/src/rules/integralflowbundles_ilp.rs +++ b/src/rules/integralflowbundles_ilp.rs @@ -27,8 +27,6 @@ impl ReductionResult for ReductionIFBToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(target_solution) } } @@ -102,13 +100,15 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/integralflowhomologousarcs_ilp.rs b/src/rules/integralflowhomologousarcs_ilp.rs index 006ecdb56..5936a804f 100644 --- a/src/rules/integralflowhomologousarcs_ilp.rs +++ b/src/rules/integralflowhomologousarcs_ilp.rs @@ -26,8 +26,6 @@ impl ReductionResult for ReductionIFHAToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(target_solution) } } @@ -105,13 +103,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/integralflowwithmultipliers_ilp.rs b/src/rules/integralflowwithmultipliers_ilp.rs index 6c700a3ba..0fe9d5447 100644 --- a/src/rules/integralflowwithmultipliers_ilp.rs +++ b/src/rules/integralflowwithmultipliers_ilp.rs @@ -26,8 +26,6 @@ impl ReductionResult for ReductionIFWMToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(target_solution) } } @@ -104,13 +102,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/isomorphicspanningtree_ilp.rs b/src/rules/isomorphicspanningtree_ilp.rs index e2d9e2ec4..e4e7b7505 100644 --- a/src/rules/isomorphicspanningtree_ilp.rs +++ b/src/rules/isomorphicspanningtree_ilp.rs @@ -28,9 +28,12 @@ impl ReductionResult for ReductionISTToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, self.n, self.n, 0) + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.n, + self.n, + 0, + )) } } @@ -97,8 +100,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( diff --git a/src/rules/kclique_balancedcompletebipartitesubgraph.rs b/src/rules/kclique_balancedcompletebipartitesubgraph.rs index b8285dbf6..2c6dd8617 100644 --- a/src/rules/kclique_balancedcompletebipartitesubgraph.rs +++ b/src/rules/kclique_balancedcompletebipartitesubgraph.rs @@ -38,8 +38,6 @@ impl ReductionResult for ReductionKCliqueToBCBS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ (0..self.num_original_vertices) .map(|v| !target_solution[v]) @@ -99,7 +97,8 @@ impl ReduceTo for KClique { } } - let graph = BipartiteGraph::new(left_size, right_size, bip_edges); + let graph = BipartiteGraph::new(left_size, right_size, bip_edges) + .map_err(>::target_construction)?; let target = BalancedCompleteBipartiteSubgraph::new(graph, target_k); Ok(ReductionKCliqueToBCBS { @@ -118,7 +117,11 @@ pub(crate) fn canonical_rule_example_specs() -> Vec select {3,4,5,6} diff --git a/src/rules/kclique_conjunctivebooleanquery.rs b/src/rules/kclique_conjunctivebooleanquery.rs index e0c66f76b..b3035a73a 100644 --- a/src/rules/kclique_conjunctivebooleanquery.rs +++ b/src/rules/kclique_conjunctivebooleanquery.rs @@ -38,8 +38,6 @@ impl ReductionResult for ReductionKCliqueToCBQ { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(KClique::::config_from_vertices( self.num_vertices, target_solution, @@ -78,7 +76,8 @@ impl ReduceTo for KClique { } } - let target = ConjunctiveBooleanQuery::new(n, vec![relation], k, conjuncts); + let target = ConjunctiveBooleanQuery::new(n, vec![relation], k, conjuncts) + .map_err(>::target_construction)?; Ok(ReductionKCliqueToCBQ { target, @@ -96,9 +95,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( source, SolutionPair { diff --git a/src/rules/kclique_ilp.rs b/src/rules/kclique_ilp.rs index 1c3e0f962..0f3dee9f0 100644 --- a/src/rules/kclique_ilp.rs +++ b/src/rules/kclique_ilp.rs @@ -43,8 +43,6 @@ impl ReductionResult for ReductionKCliqueToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -99,9 +97,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/kclique_subgraphisomorphism.rs b/src/rules/kclique_subgraphisomorphism.rs index f39f27561..e1b73dbdc 100644 --- a/src/rules/kclique_subgraphisomorphism.rs +++ b/src/rules/kclique_subgraphisomorphism.rs @@ -38,8 +38,6 @@ impl ReductionResult for ReductionKCliqueToSubIso { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(KClique::::config_from_vertices( self.num_source_vertices, target_solution, @@ -83,9 +81,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( source, SolutionPair { diff --git a/src/rules/kcoloring_bicliquecover.rs b/src/rules/kcoloring_bicliquecover.rs index 46be9a726..bc9e4d9e7 100644 --- a/src/rules/kcoloring_bicliquecover.rs +++ b/src/rules/kcoloring_bicliquecover.rs @@ -44,9 +44,6 @@ pub struct ReductionKColoringToBicliqueCover { /// the diagonal indices of each source vertex without re-reading the /// reduction parameters. num_vertices: usize, - /// Number of source colors `q`. Used as the upper bound on the number of - /// color bicliques recovered during extraction. - num_colors: usize, } impl ReductionResult for ReductionKColoringToBicliqueCover { @@ -72,14 +69,6 @@ impl ReductionResult for ReductionKColoringToBicliqueCover { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if value.0.is_none() { - return Err(crate::rules::ExtractionError::invalid( - "target configuration is not a biclique cover", - )); - } - Ok({ let n = self.num_vertices; let k = self.target.k(); @@ -91,14 +80,11 @@ impl ReductionResult for ReductionKColoringToBicliqueCover { for v in 0..n { let a_v = v; let b_v = left_size + v; - let biclique = (0..k) - .find(|&r| target_solution[r][a_v] && target_solution[r][b_v]) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "target cover leaves diagonal gadget edge {v} uncovered" - )) - })?; - diagonal_biclique.push(biclique); + diagonal_biclique.extend( + (0..k) + .filter(|&r| target_solution[r][a_v] && target_solution[r][b_v]) + .take(1), + ); } // Compact distinct biclique indices into colors 0..q-1 in first-seen order. @@ -108,12 +94,6 @@ impl ReductionResult for ReductionKColoringToBicliqueCover { for biclique in diagonal_biclique { let next_color = color_of_biclique.len(); let color = *color_of_biclique.entry(biclique).or_insert(next_color); - if color >= self.num_colors { - return Err(crate::rules::ExtractionError::invalid(format!( - "target cover uses more than {} diagonal bicliques", - self.num_colors - ))); - } coloring.push(color); } coloring @@ -143,9 +123,12 @@ impl ReduceTo for KColoring { // A loop cannot be properly colored. A single edge cannot be // covered with zero bicliques, giving a fixed NO instance. return Ok(ReductionKColoringToBicliqueCover { - target: BicliqueCover::new(BipartiteGraph::new(1, 1, vec![(0, 0)]), 0), + target: BicliqueCover::new( + BipartiteGraph::new(1, 1, vec![(0, 0)]) + .map_err(>::target_construction)?, + 0, + ), num_vertices: n, - num_colors: q, }); } @@ -211,12 +194,15 @@ impl ReduceTo for KColoring { let left_size = 2 * n; let right_size = 2 * n; - let target = BicliqueCover::new(BipartiteGraph::new(left_size, right_size, edges), n + q); + let target = BicliqueCover::new( + BipartiteGraph::new(left_size, right_size, edges) + .map_err(>::target_construction)?, + n + q, + ); Ok(ReductionKColoringToBicliqueCover { target, num_vertices: n, - num_colors: q, }) } } @@ -312,7 +298,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::with_k(SimpleGraph::new(2, vec![(0, 1)]), 2); + let source = KColoring::::with_k(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 2); let coloring = vec![0usize, 1usize]; let target_config = forward_witness(&source, &coloring); crate::example_db::specs::rule_example_with_witness::<_, BicliqueCover>( diff --git a/src/rules/kcoloring_clustering.rs b/src/rules/kcoloring_clustering.rs index 6311eb3af..3bc971fd0 100644 --- a/src/rules/kcoloring_clustering.rs +++ b/src/rules/kcoloring_clustering.rs @@ -32,8 +32,6 @@ impl ReductionResult for ReductionKColoringToClustering { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.source_num_vertices].to_vec()) } } @@ -63,7 +61,8 @@ impl ReduceTo for KColoring { fn reduce_to(&self) -> Result { Ok(ReductionKColoringToClustering { - target: Clustering::new(build_distances(self.graph()), self.num_colors(), 0), + target: Clustering::new(build_distances(self.graph()), self.num_colors(), 0) + .map_err(>::target_construction)?, source_num_vertices: self.graph().num_vertices(), }) } diff --git a/src/rules/kcoloring_partitionintocliques.rs b/src/rules/kcoloring_partitionintocliques.rs index bb3dd69bd..8479a8959 100644 --- a/src/rules/kcoloring_partitionintocliques.rs +++ b/src/rules/kcoloring_partitionintocliques.rs @@ -29,8 +29,6 @@ impl ReductionResult for ReductionKColoringToPartitionIntoCliques { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -46,9 +44,12 @@ impl ReduceTo> for KColoring fn reduce_to(&self) -> Result { let target = PartitionIntoCliques::new( - SimpleGraph::new(self.graph().num_vertices(), complement_edges(self.graph())), + SimpleGraph::new(self.graph().num_vertices(), complement_edges(self.graph())).map_err( + >>::target_construction, + )?, self.num_colors(), - ); + ) + .map_err(>>::target_construction)?; Ok(ReductionKColoringToPartitionIntoCliques { target }) } } @@ -61,7 +62,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::with_k( - SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(), 3, ); crate::example_db::specs::rule_example_with_witness::< diff --git a/src/rules/kcoloring_twodimensionalconsecutivesets.rs b/src/rules/kcoloring_twodimensionalconsecutivesets.rs index da2a2318e..39d5ed70d 100644 --- a/src/rules/kcoloring_twodimensionalconsecutivesets.rs +++ b/src/rules/kcoloring_twodimensionalconsecutivesets.rs @@ -45,14 +45,6 @@ impl ReductionResult for ReductionKColoringToTDCS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target grouping is not a consecutive-set partition", - )); - } - Ok({ // The target solution is config[symbol] = group_index. // Vertex symbols are indices 0..num_vertices. @@ -129,8 +121,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec0, 1->1, 2->2, 3->0 - let source = - KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2), (2, 3)])); + let source = KColoring::::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2), (2, 3)]).unwrap(), + ); let reduction = as ReduceTo< TwoDimensionalConsecutiveSets, >>::reduce_to(&source) diff --git a/src/rules/knapsack_ilp.rs b/src/rules/knapsack_ilp.rs index 2601ce5f9..899ee86e1 100644 --- a/src/rules/knapsack_ilp.rs +++ b/src/rules/knapsack_ilp.rs @@ -28,8 +28,6 @@ impl ReductionResult for ReductionKnapsackToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -75,7 +73,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( - Knapsack::new(vec![1, 3, 4, 5], vec![1, 4, 5, 7], 7), + Knapsack::new(vec![1, 3, 4, 5], vec![1, 4, 5, 7], 7).unwrap(), SolutionPair { source_config: serde_json::json!(vec![false, true, true, false]), target_config: serde_json::json!(vec![0, 1, 1, 0]), diff --git a/src/rules/knapsack_qubo.rs b/src/rules/knapsack_qubo.rs index 63a0e208d..f7c7540be 100644 --- a/src/rules/knapsack_qubo.rs +++ b/src/rules/knapsack_qubo.rs @@ -38,8 +38,6 @@ impl ReductionResult for ReductionKnapsackToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_items].to_vec()) } } @@ -108,7 +106,7 @@ impl ReduceTo> for Knapsack { coeffs[n + j] = weight; } - let mut matrix = vec![vec![0_i64; total]; total]; + let mut matrix = vec![std::collections::BTreeMap::new(); total]; // Diagonal: P * a_k^2 - 2P * C * a_k - v_k (for items) for k in 0..total { @@ -121,29 +119,33 @@ impl ReduceTo> for Knapsack { .and_then(|value| value.checked_mul(coeffs[k])) .and_then(|value| value.checked_mul(2)) .ok_or_else(|| overflow("computing a knapsack QUBO linear penalty"))?; - matrix[k][k] = square + let mut diagonal = square .checked_sub(linear) .ok_or_else(|| overflow("combining knapsack QUBO diagonal penalties"))?; if k < n { - matrix[k][k] = matrix[k][k] + diagonal = diagonal .checked_sub(values[k]) .ok_or_else(|| overflow("adding a knapsack value to the QUBO objective"))?; } + matrix[k].insert(k, diagonal); } // Off-diagonal (upper triangular): 2P * a_i * a_j for i in 0..total { for j in (i + 1)..total { - matrix[i][j] = penalty - .checked_mul(coeffs[i]) - .and_then(|value| value.checked_mul(coeffs[j])) - .and_then(|value| value.checked_mul(2)) - .ok_or_else(|| overflow("computing a knapsack QUBO interaction"))?; + matrix[i].insert( + j, + penalty + .checked_mul(coeffs[i]) + .and_then(|value| value.checked_mul(coeffs[j])) + .and_then(|value| value.checked_mul(2)) + .ok_or_else(|| overflow("computing a knapsack QUBO interaction"))?, + ); } } Ok(ReductionKnapsackToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { + target: QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::>(message) })?, num_items: n, @@ -159,7 +161,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( - Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7), + Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7).unwrap(), SolutionPair { source_config: serde_json::json!(vec![true, false, false, true]), target_config: serde_json::json!(vec![ diff --git a/src/rules/ksatisfiability_acyclicpartition.rs b/src/rules/ksatisfiability_acyclicpartition.rs index 257516807..1b98a37c5 100644 --- a/src/rules/ksatisfiability_acyclicpartition.rs +++ b/src/rules/ksatisfiability_acyclicpartition.rs @@ -33,13 +33,6 @@ impl ReductionResult for Reduction3SATToAcyclicPartition { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - if !crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)? - .0 - { - return Err(crate::rules::ExtractionError::invalid( - "target partition does not satisfy the acyclic partition constraints", - )); - } let source_label = target_solution[self.source_vertex]; let selected = target_solution[..self.sat_to_clique.target_problem().num_vertices()] .iter() @@ -103,12 +96,14 @@ impl ReduceTo> for KSatisfiability { )?, ); let target = AcyclicPartition::new( - DirectedGraph::new(target_n, arcs), + DirectedGraph::new(target_n, arcs) + .map_err(>>::target_construction)?, weights, arc_costs, weight_bound, cost_bound, - ); + ) + .map_err(>>::target_construction)?; Ok(Reduction3SATToAcyclicPartition { sat_to_clique, target, diff --git a/src/rules/ksatisfiability_bicliquecover.rs b/src/rules/ksatisfiability_bicliquecover.rs index 5caf355a0..1f3d515e4 100644 --- a/src/rules/ksatisfiability_bicliquecover.rs +++ b/src/rules/ksatisfiability_bicliquecover.rs @@ -89,18 +89,11 @@ impl ReductionResult for ReductionKSatisfiabilityToBicliqueCover { /// The rank budget forces a unique row covering the first domino anchor. /// Its left crown memberships give the normalized truth assignment. /// Map appearing variables back to their original indices and assign false - /// to variables absent from the formula. Infeasible covers are rejected. + /// to variables absent from the formula. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if value.0.is_none() { - return Err(crate::rules::ExtractionError::invalid( - "target configuration is not a biclique cover", - )); - } // Variables absent from every clause may be assigned false. // This also defines the inverse map for the empty-formula YES target. let mut source_assignment = vec![false; self.source_num_vars]; @@ -111,18 +104,14 @@ impl ReductionResult for ReductionKSatisfiabilityToBicliqueCover { let s11_v = self.target.left_size() + self.s1_right_offset; // The Y matching and the important induced matching use the entire // rank budget. Exactly one row covers this important anchor edge. - let b1_index = target_solution + for row in target_solution .iter() - .position(|row| row[s11_u] && row[s11_v]); - - let b1_index = b1_index.ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "target configuration has no important-edge biclique B_1", - ) - })?; - // Pair i corresponds to source_variables[i]; its t variable is 2*i. - for (i, &source_index) in self.source_variables.iter().enumerate() { - source_assignment[source_index] = target_solution[b1_index][2 * i]; + .filter(|row| row[s11_u] && row[s11_v]) + { + // Pair i corresponds to source_variables[i]; its t variable is 2*i. + for (i, &source_index) in self.source_variables.iter().enumerate() { + source_assignment[source_index] = row[2 * i]; + } } Ok(source_assignment) } @@ -264,7 +253,11 @@ impl ReduceTo for KSatisfiability { vec![] }; return Ok(ReductionKSatisfiabilityToBicliqueCover { - target: BicliqueCover::new(BipartiteGraph::new(size, size, edges), 0), + target: BicliqueCover::new( + BipartiteGraph::new(size, size, edges) + .map_err(>::target_construction)?, + 0, + ), source_num_vars, normalized_n: 0, s1_left_offset: 0, @@ -497,7 +490,8 @@ impl ReduceTo for KSatisfiability { // ---------------- Assemble target ---------------- let edges_vec: Vec<(usize, usize)> = edges.into_iter().collect(); - let bipartite = BipartiteGraph::new(partition_size, partition_size, edges_vec); + let bipartite = BipartiteGraph::new(partition_size, partition_size, edges_vec) + .map_err(>::target_construction)?; let target = BicliqueCover::new(bipartite, rank); Ok(ReductionKSatisfiabilityToBicliqueCover { diff --git a/src/rules/ksatisfiability_cyclicordering.rs b/src/rules/ksatisfiability_cyclicordering.rs index 704a26f66..7a43dc164 100644 --- a/src/rules/ksatisfiability_cyclicordering.rs +++ b/src/rules/ksatisfiability_cyclicordering.rs @@ -43,13 +43,6 @@ impl ReductionResult for Reduction3SATToCyclicOrdering { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target configuration is not a feasible cyclic ordering", - )); - } let mut assignment = vec![false; self.source_num_vars]; for (compact, &original) in self.source_variables.iter().enumerate() { let (alpha, beta, gamma) = variable_triple(compact); diff --git a/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs b/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs index a9e5d8bcb..14bdabe93 100644 --- a/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs +++ b/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs @@ -175,8 +175,6 @@ impl ReductionResult for Reduction3SATToDirectedTwoCommodityIntegralFlow { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ self.variable_paths .iter() @@ -300,7 +298,9 @@ impl ReduceTo for KSatisfiability { >("converting the clause count to an i64 flow requirement") })?; let target = DirectedTwoCommodityIntegralFlow::new( - DirectedGraph::new(next_vertex, arcs), + DirectedGraph::new(next_vertex, arcs).map_err( + >::target_construction, + )?, capacities, source_1, sink_1, @@ -308,7 +308,8 @@ impl ReduceTo for KSatisfiability { sink_2, 1, clause_requirement, - ); + ) + .map_err(>::target_construction)?; Ok(Reduction3SATToDirectedTwoCommodityIntegralFlow { target, diff --git a/src/rules/ksatisfiability_feasibleregisterassignment.rs b/src/rules/ksatisfiability_feasibleregisterassignment.rs index 23176c5ed..3ea29de21 100644 --- a/src/rules/ksatisfiability_feasibleregisterassignment.rs +++ b/src/rules/ksatisfiability_feasibleregisterassignment.rs @@ -78,13 +78,6 @@ impl ReductionResult for Reduction3SATToFeasibleRegisterAssignment { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target configuration is not a feasible register assignment realization", - )); - } let mut assignment = vec![false; self.num_vars]; let compact_vars = self.source_variables.len(); for (compact, &original) in self.source_variables.iter().enumerate() { @@ -115,7 +108,8 @@ impl ReduceTo for KSatisfiability { // Both predecessors must remain live until vertex 2, but they // share a register. This acyclic target has no realization. return Ok(Reduction3SATToFeasibleRegisterAssignment { - target: FeasibleRegisterAssignment::new(3, vec![(2, 0), (2, 1)], 2, vec![0, 0, 1]), + target: FeasibleRegisterAssignment::new(3, vec![(2, 0), (2, 1)], 2, vec![0, 0, 1]) + .map_err(>::target_construction)?, num_vars: self.num_vars(), source_variables: Vec::new(), }); @@ -219,7 +213,8 @@ impl ReduceTo for KSatisfiability { } Ok(Reduction3SATToFeasibleRegisterAssignment { - target: FeasibleRegisterAssignment::new(num_vertices, arcs, num_registers, assignment), + target: FeasibleRegisterAssignment::new(num_vertices, arcs, num_registers, assignment) + .map_err(>::target_construction)?, num_vars: self.num_vars(), source_variables, }) diff --git a/src/rules/ksatisfiability_kclique.rs b/src/rules/ksatisfiability_kclique.rs index 19c92416b..d8ff8f6f6 100644 --- a/src/rules/ksatisfiability_kclique.rs +++ b/src/rules/ksatisfiability_kclique.rs @@ -33,13 +33,6 @@ impl ReductionResult for Reduction3SATToKClique { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - if !crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)? - .0 - { - return Err(crate::rules::ExtractionError::invalid( - "target selection is not a clique meeting the threshold", - )); - } // Variables absent from the selected literals are free; choose false. let mut assignment = vec![false; self.source_num_vars]; for (&selected, &(variable, positive)) in target_solution[..self.literal_assignments.len()] @@ -90,7 +83,12 @@ impl ReduceTo> for KSatisfiability { } let anchor = positions.len(); edges.extend((0..anchor).map(|v| (v, anchor))); - let target = KClique::new(SimpleGraph::new(num_vertices, edges), k); + let target = KClique::new( + SimpleGraph::new(num_vertices, edges) + .map_err(>>::target_construction)?, + k, + ) + .map_err(>>::target_construction)?; Ok(Reduction3SATToKClique { target, literal_assignments: positions.into_iter().map(|(_, v, p)| (v, p)).collect(), diff --git a/src/rules/ksatisfiability_kernel.rs b/src/rules/ksatisfiability_kernel.rs index d426e0025..69c6714e3 100644 --- a/src/rules/ksatisfiability_kernel.rs +++ b/src/rules/ksatisfiability_kernel.rs @@ -33,13 +33,6 @@ impl ReductionResult for Reduction3SatToKernel { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target vertex selection is not a kernel", - )); - } let mut assignment = vec![false; self.source_num_vars]; for (compact, &original) in self.source_variables.iter().enumerate() { assignment[original] = target_solution[2 * compact]; @@ -113,7 +106,10 @@ impl ReduceTo for KSatisfiability { } Ok(Reduction3SatToKernel { - target: Kernel::new(DirectedGraph::new(num_vertices, arcs)), + target: Kernel::new( + DirectedGraph::new(num_vertices, arcs) + .map_err(>::target_construction)?, + ), source_num_vars: self.num_vars(), source_variables, }) diff --git a/src/rules/ksatisfiability_minimumvertexcover.rs b/src/rules/ksatisfiability_minimumvertexcover.rs index 38d133bbe..7394cd337 100644 --- a/src/rules/ksatisfiability_minimumvertexcover.rs +++ b/src/rules/ksatisfiability_minimumvertexcover.rs @@ -23,6 +23,7 @@ use crate::variant::K3; pub struct Reduction3SATToMVC { target: MinimumVertexCover, source_num_vars: usize, + target_bound: i64, } impl ReductionResult for Reduction3SATToMVC { @@ -37,15 +38,13 @@ impl ReductionResult for Reduction3SATToMVC { /// /// Vertex layout: indices 0..2n are literal vertices (even = positive, /// odd = negated). For variable i, vertex 2*i is u_i and vertex 2*i+1 - /// is not-u_i. Each truth-setting edge forces exactly one of these two - /// into any minimum vertex cover. If u_i is in the cover, set x_i = 1; + /// is not-u_i. A cover meeting the n + 2m bound contains exactly one of these two + /// for each variable. If u_i is in the cover, set x_i = 1; /// if not-u_i is in the cover, set x_i = 0. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ (0..self.source_num_vars) .map(|i| { @@ -57,7 +56,21 @@ impl ReductionResult for Reduction3SATToMVC { } } +impl crate::rules::AggregateReductionResult for Reduction3SATToMVC { + type Source = KSatisfiability; + type Target = MinimumVertexCover; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { + crate::types::Or(value.0.is_some_and(|cost| cost <= self.target_bound)) + } +} + #[reduction( + aggregate = custom, transform = exact { num_vertices = "2 * num_vars + 3 * num_clauses", num_edges = "num_vars + 6 * num_clauses", @@ -69,6 +82,16 @@ impl ReduceTo> for KSatisfiability { fn reduce_to(&self) -> Result { let n = self.num_vars(); let m = self.num_clauses(); + let target_bound = m + .checked_mul(2) + .and_then(|value| value.checked_add(n)) + .and_then(|value| i64::try_from(value).ok()) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::< + KSatisfiability, + MinimumVertexCover, + >("computing the target cover bound") + })?; let total_vertices = 2 * n + 3 * m; let mut edges: Vec<(usize, usize)> = Vec::with_capacity(n + 6 * m); @@ -100,13 +123,18 @@ impl ReduceTo> for KSatisfiability { } } - let graph = SimpleGraph::new(total_vertices, edges); + let graph = SimpleGraph::new(total_vertices, edges).map_err( + >>::target_construction, + )?; let weights = vec![1i64; total_vertices]; - let target = MinimumVertexCover::new(graph, weights); + let target = MinimumVertexCover::new(graph, weights).map_err( + >>::target_construction, + )?; Ok(Reduction3SATToMVC { target, source_num_vars: n, + target_bound, }) } } diff --git a/src/rules/ksatisfiability_monochromatictriangle.rs b/src/rules/ksatisfiability_monochromatictriangle.rs index 61a8de78a..18d72fec6 100644 --- a/src/rules/ksatisfiability_monochromatictriangle.rs +++ b/src/rules/ksatisfiability_monochromatictriangle.rs @@ -55,7 +55,6 @@ impl ReductionResult for Reduction3SATToMonochromaticTriangle { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; let nae_solution = (0..self.nae_reduction.target_problem().num_vars()) .map(|index| target_solution[2 * index]) .collect(); @@ -154,7 +153,9 @@ impl ReduceTo> for KSatisfiability { } debug_assert_eq!(next_vertex, num_vertices); debug_assert_eq!(edges.len(), num_edges); - let target = MonochromaticTriangle::new(SimpleGraph::new(num_vertices, edges)); + let target = MonochromaticTriangle::new(SimpleGraph::new(num_vertices, edges).map_err( + >>::target_construction, + )?); Ok(Reduction3SATToMonochromaticTriangle { target, nae_reduction, diff --git a/src/rules/ksatisfiability_oneinthreesatisfiability.rs b/src/rules/ksatisfiability_oneinthreesatisfiability.rs index 3c9faa76c..e8e1fb4b6 100644 --- a/src/rules/ksatisfiability_oneinthreesatisfiability.rs +++ b/src/rules/ksatisfiability_oneinthreesatisfiability.rs @@ -31,13 +31,6 @@ impl ReductionResult for Reduction3SATToOneInThreeSAT { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target assignment does not satisfy every one-in-three clause", - )); - } let mut assignment = vec![false; self.source_num_vars]; for (compact, &original) in self.source_variables.iter().enumerate() { assignment[original] = target_solution[compact]; diff --git a/src/rules/ksatisfiability_preemptivescheduling.rs b/src/rules/ksatisfiability_preemptivescheduling.rs index e994b9cda..dd378edad 100644 --- a/src/rules/ksatisfiability_preemptivescheduling.rs +++ b/src/rules/ksatisfiability_preemptivescheduling.rs @@ -339,8 +339,6 @@ impl ReductionResult for Reduction3SATToPreemptiveScheduling { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let d_max = self.target.d_max(); self.positive_start_jobs diff --git a/src/rules/ksatisfiability_quadraticcongruences.rs b/src/rules/ksatisfiability_quadraticcongruences.rs index 15320498f..f1ac10d28 100644 --- a/src/rules/ksatisfiability_quadraticcongruences.rs +++ b/src/rules/ksatisfiability_quadraticcongruences.rs @@ -40,13 +40,6 @@ impl ReductionResult for Reduction3SATToQuadraticCongruences { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target integer does not satisfy the bounded quadratic congruence", - )); - } // Validation gives 0 < x <= H. Each prime power divides exactly one // of H-x and H+x. The coordinate zero sign chooses x or -x so that // the odd linear target, rather than its negative, is recovered. diff --git a/src/rules/ksatisfiability_quadraticdiophantineequations.rs b/src/rules/ksatisfiability_quadraticdiophantineequations.rs index e8e4053c8..f6e8b9cb8 100644 --- a/src/rules/ksatisfiability_quadraticdiophantineequations.rs +++ b/src/rules/ksatisfiability_quadraticdiophantineequations.rs @@ -32,8 +32,6 @@ impl ReductionResult for Reduction3SATToQuadraticDiophantineEquations { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ self.congruence_reduction .extract_solution(target_solution)? diff --git a/src/rules/ksatisfiability_qubo.rs b/src/rules/ksatisfiability_qubo.rs index 4a7b8b21c..47adbae75 100644 --- a/src/rules/ksatisfiability_qubo.rs +++ b/src/rules/ksatisfiability_qubo.rs @@ -37,13 +37,6 @@ impl ReductionResult for ReductionKSatToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "QUBO energy does not meet the SAT zero-penalty threshold", - )); - } Ok(target_solution[..self.source_num_vars].to_vec()) } } @@ -68,13 +61,6 @@ impl ReductionResult for Reduction3SATToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "QUBO energy does not meet the SAT zero-penalty threshold", - )); - } Ok(target_solution[..self.source_num_vars].to_vec()) } } @@ -84,19 +70,20 @@ impl ReductionResult for Reduction3SATToQUBO { /// For clause (l_i ∨ l_j), the penalty for the clause being unsatisfied is /// the product of the complemented literals. fn add_coefficient( - matrix: &mut [Vec], + matrix: &mut [std::collections::BTreeMap], row: usize, column: usize, coefficient: i64, ) -> Result<(), &'static str> { - matrix[row][column] = matrix[row][column] + let entry = matrix[row].entry(column).or_insert(0i64); + *entry = entry .checked_add(coefficient) .ok_or("adding a SAT QUBO coefficient")?; Ok(()) } fn add_2sat_clause_penalty( - matrix: &mut [Vec], + matrix: &mut [std::collections::BTreeMap], lits: &[(usize, bool)], ) -> Result<(), &'static str> { assert_eq!(lits.len(), 2, "Expected 2-literal clause"); @@ -151,7 +138,7 @@ fn add_2sat_clause_penalty( /// /// `aux_var` is the 0-indexed auxiliary variable. fn add_3sat_clause_penalty( - matrix: &mut [Vec], + matrix: &mut [std::collections::BTreeMap], lits: &[(usize, bool)], aux_var: usize, ) -> Result<(), &'static str> { @@ -176,7 +163,7 @@ fn add_3sat_clause_penalty( // Helper: add coefficient * yi * yj to the matrix // where yi depends on variable vi and negation ni - let add_yy = |matrix: &mut [Vec], + let add_yy = |matrix: &mut [std::collections::BTreeMap], vi: usize, ni: bool, vj: usize, @@ -241,7 +228,7 @@ fn add_3sat_clause_penalty( // Helper: add coefficient * yi * a to the matrix // where yi depends on variable vi and negation ni, a is aux variable - let add_ya = |matrix: &mut [Vec], + let add_ya = |matrix: &mut [std::collections::BTreeMap], vi: usize, ni: bool, a: usize, @@ -280,6 +267,8 @@ fn add_3sat_clause_penalty( Ok(()) } +type CoefficientRows = Vec>; + /// Expand clause penalties and retain the constant omitted by QUBO. /// K3 reserves one auxiliary per clause, including free auxiliaries for short /// clauses; K2 reserves none. The source constructor validates clause widths. @@ -287,14 +276,11 @@ fn build_qubo_matrix( num_vars: usize, clauses: &[crate::models::formula::CNFClause], num_aux: usize, -) -> Result<(Vec>, i64), &'static str> { +) -> Result<(CoefficientRows, i64), &'static str> { let total = num_vars .checked_add(num_aux) .ok_or("computing the number of SAT QUBO variables")?; - total - .checked_mul(total) - .ok_or("computing the SAT QUBO dense matrix entry count")?; - let mut matrix = vec![vec![0; total]; total]; + let mut matrix = vec![std::collections::BTreeMap::new(); total]; let mut constant = 0i64; for (idx, clause) in clauses.iter().enumerate() { let literals: Vec<_> = clause @@ -366,7 +352,7 @@ impl ReduceTo> for KSatisfiability { })?; Ok(ReductionKSatToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { + target: QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::, QUBO>( message, ) @@ -396,7 +382,7 @@ impl ReduceTo> for KSatisfiability { })?; Ok(Reduction3SATToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { + target: QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::, QUBO>( message, ) diff --git a/src/rules/ksatisfiability_registersufficiency.rs b/src/rules/ksatisfiability_registersufficiency.rs index 8cde3e058..5d8e4bf75 100644 --- a/src/rules/ksatisfiability_registersufficiency.rs +++ b/src/rules/ksatisfiability_registersufficiency.rs @@ -296,13 +296,6 @@ impl ReductionResult for Reduction3SATToRegisterSufficiency { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target ordering does not satisfy the register bound and dependencies", - )); - } let mut assignment = vec![false; self.source_num_vars]; let Some(layout) = &self.layout else { // Only the empty-conjunction target has a feasible witness here. @@ -311,12 +304,6 @@ impl ReductionResult for Reduction3SATToRegisterSufficiency { let cutoff = target_solution[layout.w(layout.num_vars - 1)]; for (variable, &original) in self.source_variables.iter().enumerate() { let positive = target_solution[layout.x_pos(variable)] < cutoff; - let negative = target_solution[layout.x_neg(variable)] < cutoff; - if positive && negative { - return Err(crate::rules::ExtractionError::invalid(format!( - "both literals of variable {original} precede the extraction cutoff" - ))); - } assignment[original] = positive; } Ok(assignment) @@ -343,7 +330,8 @@ impl ReduceTo for KSatisfiability { // Zero vertices need zero registers (YES); one output vertex // cannot be computed with zero registers (NO). return Ok(Reduction3SATToRegisterSufficiency { - target: RegisterSufficiency::new(usize::from(empty_clause), Vec::new(), 0), + target: RegisterSufficiency::new(usize::from(empty_clause), Vec::new(), 0) + .map_err(>::target_construction)?, layout: None, source_num_vars: self.num_vars(), source_variables: Vec::new(), @@ -501,7 +489,8 @@ impl ReduceTo for KSatisfiability { } Ok(Reduction3SATToRegisterSufficiency { - target: RegisterSufficiency::new(layout.total_vertices(), arcs, layout.bound()), + target: RegisterSufficiency::new(layout.total_vertices(), arcs, layout.bound()) + .map_err(>::target_construction)?, layout: Some(layout), source_num_vars: self.num_vars(), source_variables, diff --git a/src/rules/ksatisfiability_simultaneousincongruences.rs b/src/rules/ksatisfiability_simultaneousincongruences.rs index 785e808b8..8d6b58b99 100644 --- a/src/rules/ksatisfiability_simultaneousincongruences.rs +++ b/src/rules/ksatisfiability_simultaneousincongruences.rs @@ -30,14 +30,8 @@ impl ReductionResult for Reduction3SATToSimultaneousIncongruences { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ - let x = u64::try_from(*target_solution).map_err(|_| { - crate::rules::ExtractionError::invalid( - "target value cannot be represented in the CRT implementation domain", - ) - })?; + let x = *target_solution as u64; self.variable_primes .iter() .map(|&prime| x % prime == 1) diff --git a/src/rules/ksatisfiability_subsetsum.rs b/src/rules/ksatisfiability_subsetsum.rs index 3c62e5d14..cb780b17a 100644 --- a/src/rules/ksatisfiability_subsetsum.rs +++ b/src/rules/ksatisfiability_subsetsum.rs @@ -39,8 +39,6 @@ impl ReductionResult for Reduction3SATToSubsetSum { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Variable integers are the first 2n elements in 0-based indexing: // for variable i (0 <= i < n), y_i is stored at index 2*i and z_i at index 2*i + 1. diff --git a/src/rules/ksatisfiability_timetabledesign.rs b/src/rules/ksatisfiability_timetabledesign.rs index 112eabbb6..c8a0e99bc 100644 --- a/src/rules/ksatisfiability_timetabledesign.rs +++ b/src/rules/ksatisfiability_timetabledesign.rs @@ -748,8 +748,6 @@ impl ReductionResult for Reduction3SATToTimetableDesign { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let num_periods = self.target.num_periods(); @@ -805,7 +803,8 @@ impl ReduceTo for KSatisfiability { layout.craftsman_avail.clone(), layout.task_avail.clone(), layout.requirements.clone(), - ); + ) + .map_err(>::target_construction)?; Ok(Reduction3SATToTimetableDesign { target, layout }) } diff --git a/src/rules/lengthboundeddisjointpaths_ilp.rs b/src/rules/lengthboundeddisjointpaths_ilp.rs index 5745f463e..2e8d01295 100644 --- a/src/rules/lengthboundeddisjointpaths_ilp.rs +++ b/src/rules/lengthboundeddisjointpaths_ilp.rs @@ -40,22 +40,12 @@ impl ReductionResult for ReductionLBDPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let m = self.edges.len(); let flow_vars_per_k = 2 * m; let activation_offset = self.num_paths * flow_vars_per_k; let mut result = vec![vec![false; m]; self.num_paths]; for (k, path) in result.iter_mut().enumerate() { if target_solution[activation_offset + k] == 0 { - if target_solution[k * flow_vars_per_k..(k + 1) * flow_vars_per_k] - .iter() - .any(|&flow| flow != 0) - { - return Err(crate::rules::ExtractionError::invalid( - "inactive path slot contains flow", - )); - } continue; } let mut adjacency = vec![Vec::new(); self.num_vertices]; @@ -86,12 +76,7 @@ impl ReductionResult for ReductionLBDPToILP { } } let mut vertex = self.sink; - while vertex != self.source { - let (previous, edge) = predecessor[vertex].ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "active path flow does not connect source to sink", - ) - })?; + while let Some((previous, edge)) = predecessor[vertex] { path[edge] = true; vertex = previous; } @@ -246,11 +231,12 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/longestcircuit_ilp.rs b/src/rules/longestcircuit_ilp.rs index c67c9a2e7..bedae03c0 100644 --- a/src/rules/longestcircuit_ilp.rs +++ b/src/rules/longestcircuit_ilp.rs @@ -40,8 +40,6 @@ impl ReductionResult for ReductionLongestCircuitToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_edges] .iter() .map(|&value| value == 1) @@ -181,9 +179,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/longestcommonsubsequence_ilp.rs b/src/rules/longestcommonsubsequence_ilp.rs index 11225f470..0d88e131e 100644 --- a/src/rules/longestcommonsubsequence_ilp.rs +++ b/src/rules/longestcommonsubsequence_ilp.rs @@ -35,14 +35,12 @@ impl ReductionResult for ReductionLCSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.max_length, self.alphabet_size + 1, 0, - )? + ) .into_iter() .map(|symbol| (symbol < self.alphabet_size).then_some(symbol)) .collect()) @@ -177,7 +175,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/longestcommonsubsequence_maximumindependentset.rs b/src/rules/longestcommonsubsequence_maximumindependentset.rs index be5454a14..f2f90fa14 100644 --- a/src/rules/longestcommonsubsequence_maximumindependentset.rs +++ b/src/rules/longestcommonsubsequence_maximumindependentset.rs @@ -50,8 +50,6 @@ impl ReductionResult for ReductionLCSToIS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Collect selected match nodes with their characters let mut selected: Vec<(usize, usize)> = target_solution @@ -134,9 +132,14 @@ impl ReduceTo> for LongestCommonSubseque } let target = MaximumIndependentSet::new( - SimpleGraph::new(num_vertices, edges), + SimpleGraph::new(num_vertices, edges).map_err( + >>::target_construction, + )?, vec![One; num_vertices], - ); + ) + .map_err( + >>::target_construction, + )?; Ok(ReductionLCSToIS { target, @@ -200,6 +203,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ (0..self.num_edges) .map(|edge_idx| { @@ -185,8 +183,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/maxcut_minimumcutintoboundedsets.rs b/src/rules/maxcut_minimumcutintoboundedsets.rs index 795c46ffb..869285185 100644 --- a/src/rules/maxcut_minimumcutintoboundedsets.rs +++ b/src/rules/maxcut_minimumcutintoboundedsets.rs @@ -34,8 +34,6 @@ impl ReductionResult for ReductionMaxCutToMinCutBounded { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.original_n].to_vec()) } } @@ -121,12 +119,12 @@ impl ReduceTo> for MaxCut>>::target_construction)?, weights, source_vertex, sink_vertex, size_bound, - ); + ).map_err(>>::target_construction)?; Ok(ReductionMaxCutToMinCutBounded { target, @@ -145,9 +143,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source) .expect("reduction should succeed"); diff --git a/src/rules/maxcut_minimummatrixcover.rs b/src/rules/maxcut_minimummatrixcover.rs index a1e8f50c7..af147f7fd 100644 --- a/src/rules/maxcut_minimummatrixcover.rs +++ b/src/rules/maxcut_minimummatrixcover.rs @@ -13,9 +13,9 @@ //! sign encoding (`config[i] = 1 ⇔ f(i) = +1 ⇔ i ∈ S`). //! //! **Precondition:** all edge weights must be nonnegative. The reduction -//! panics on any negative weight, since `MinimumMatrixCover` requires a +//! returns an error on any negative weight, since `MinimumMatrixCover` requires a //! nonnegative integer matrix. Negative-weight `MaxCut` instances are out -//! of scope and must use a different (preprocessing) reduction. +//! of scope for this reduction. //! //! Reference: Garey & Johnson, *Computers and Intractability* (1979), //! Appendix A1.2, MS13 ("Transformation from MAXIMUM CUT"). @@ -52,8 +52,6 @@ impl ReductionResult for ReductionMaxCutToMMC { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -79,13 +77,13 @@ impl ReduceTo for MaxCut { "edge ({u}, {v}) has negative weight {w}" ))); } - let w64 = w; - matrix[u][v] = w64; - matrix[v][u] = w64; + matrix[u][v] = w; + matrix[v][u] = w; } Ok(ReductionMaxCutToMMC { - target: MinimumMatrixCover::new(matrix), + target: MinimumMatrixCover::new(matrix) + .map_err(>::target_construction)?, }) } } @@ -101,9 +99,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]).unwrap(), vec![1, 1, 1, 1], - ); + ) + .unwrap(); crate::example_db::specs::rule_example_with_witness::<_, MinimumMatrixCover>( source, SolutionPair { diff --git a/src/rules/maximalis_ilp.rs b/src/rules/maximalis_ilp.rs index 1d2e3f93c..f8fb78ba6 100644 --- a/src/rules/maximalis_ilp.rs +++ b/src/rules/maximalis_ilp.rs @@ -26,8 +26,6 @@ impl ReductionResult for ReductionMxISToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -86,7 +84,11 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/maximum2satisfiability_ilp.rs b/src/rules/maximum2satisfiability_ilp.rs index cfa12df13..76fbc49cc 100644 --- a/src/rules/maximum2satisfiability_ilp.rs +++ b/src/rules/maximum2satisfiability_ilp.rs @@ -31,8 +31,6 @@ impl ReductionResult for ReductionMaximum2SatisfiabilityToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vars] .iter() .map(|&value| value == 1) diff --git a/src/rules/maximum2satisfiability_maxcut.rs b/src/rules/maximum2satisfiability_maxcut.rs index e153b5d4f..c3830caaa 100644 --- a/src/rules/maximum2satisfiability_maxcut.rs +++ b/src/rules/maximum2satisfiability_maxcut.rs @@ -37,8 +37,6 @@ impl ReductionResult for ReductionMaximum2SatisfiabilityToMaxCut { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let reference_side = target_solution[0]; (0..self.source_num_vars) @@ -93,7 +91,12 @@ impl ReduceTo> for Maximum2Satisfiability { .filter(|(_, weight)| *weight != 0) .unzip(); - let target = MaxCut::new(SimpleGraph::new(self.num_vars() + 1, edges), weights); + let target = MaxCut::new( + SimpleGraph::new(self.num_vars() + 1, edges) + .map_err(>>::target_construction)?, + weights, + ) + .map_err(>>::target_construction)?; Ok(ReductionMaximum2SatisfiabilityToMaxCut { target, diff --git a/src/rules/maximumclique_ilp.rs b/src/rules/maximumclique_ilp.rs index 7f62e15b1..223f6cffc 100644 --- a/src/rules/maximumclique_ilp.rs +++ b/src/rules/maximumclique_ilp.rs @@ -39,8 +39,6 @@ impl ReductionResult for ReductionCliqueToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -93,7 +91,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/maximumclique_maximumindependentset.rs b/src/rules/maximumclique_maximumindependentset.rs index 7a42f27f4..bbd95ea32 100644 --- a/src/rules/maximumclique_maximumindependentset.rs +++ b/src/rules/maximumclique_maximumindependentset.rs @@ -32,21 +32,19 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } fn reduce_clique_to_is( src: &MaximumClique, -) -> ReductionCliqueToIS { +) -> Result, crate::registry::ConstructionError> { let comp_edges = super::graph_helpers::complement_edges(src.graph()); let target = MaximumIndependentSet::new( - SimpleGraph::new(src.graph().num_vertices(), comp_edges), + SimpleGraph::new(src.graph().num_vertices(), comp_edges)?, src.weights().to_vec(), - ); - ReductionCliqueToIS { target } + )?; + Ok(ReductionCliqueToIS { target }) } #[reduction( @@ -59,7 +57,9 @@ impl ReduceTo> for MaximumClique; fn reduce_to(&self) -> Result { - Ok(reduce_clique_to_is(self)) + reduce_clique_to_is(self).map_err( + >>::target_construction, + ) } } @@ -73,7 +73,9 @@ impl ReduceTo> for MaximumClique; fn reduce_to(&self) -> Result { - Ok(reduce_clique_to_is(self)) + reduce_clique_to_is(self).map_err( + >>::target_construction, + ) } } @@ -86,9 +88,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, @@ -105,9 +108,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, diff --git a/src/rules/maximumcokplex_ilp.rs b/src/rules/maximumcokplex_ilp.rs index 8021bb316..aaa34620b 100644 --- a/src/rules/maximumcokplex_ilp.rs +++ b/src/rules/maximumcokplex_ilp.rs @@ -35,8 +35,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -145,10 +143,11 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::with_k( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(), vec![5, 1, 4, 1, 3], 2, - ); + ) + .unwrap(); crate::example_db::specs::rule_example_via_ilp::<_, bool>(source) }, }, @@ -156,10 +155,11 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::with_k( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(), vec![One; 5], 2, - ); + ) + .unwrap(); crate::example_db::specs::rule_example_via_ilp::<_, bool>(source) }, }, diff --git a/src/rules/maximumcommonedgesubgraph_ilp.rs b/src/rules/maximumcommonedgesubgraph_ilp.rs index 4ab7d8858..e5dd969ac 100644 --- a/src/rules/maximumcommonedgesubgraph_ilp.rs +++ b/src/rules/maximumcommonedgesubgraph_ilp.rs @@ -47,22 +47,15 @@ impl ReductionResult for ReductionMCESToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let n2 = self.num_vertices_2; - (0..self.num_vertices_1) + Ok((0..self.num_vertices_1) .map(|vertex| { - let mut selected = - (0..n2).filter(|&mapped| target_solution[vertex * n2 + mapped] == 1); - match (selected.next(), selected.next()) { - (Some(mapped), None) => Ok(mapped), - (None, _) => Ok(n2), - (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( - "source vertex {vertex} maps to multiple target vertices" - ))), + match (0..n2).find(|&mapped| target_solution[vertex * n2 + mapped] == 1) { + Some(mapped) => mapped, + None => n2, } }) - .collect() + .collect()) } } @@ -152,11 +145,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, diff --git a/src/rules/maximumcontactmapoverlap_ilp.rs b/src/rules/maximumcontactmapoverlap_ilp.rs index 709e0053f..150309bfa 100644 --- a/src/rules/maximumcontactmapoverlap_ilp.rs +++ b/src/rules/maximumcontactmapoverlap_ilp.rs @@ -50,22 +50,15 @@ impl ReductionResult for ReductionCMOToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let n2 = self.num_vertices_2; - (0..self.num_vertices_1) + Ok((0..self.num_vertices_1) .map(|residue| { - let mut selected = - (0..n2).filter(|&mapped| target_solution[residue * n2 + mapped] == 1); - match (selected.next(), selected.next()) { - (Some(mapped), None) => Ok(mapped + 1), - (None, _) => Ok(0), - (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( - "source residue {residue} maps to multiple target residues" - ))), + match (0..n2).find(|&mapped| target_solution[residue * n2 + mapped] == 1) { + Some(mapped) => mapped + 1, + None => 0, } }) - .collect() + .collect()) } } diff --git a/src/rules/maximumdomaticnumber_ilp.rs b/src/rules/maximumdomaticnumber_ilp.rs index 3a0221a6e..78237a665 100644 --- a/src/rules/maximumdomaticnumber_ilp.rs +++ b/src/rules/maximumdomaticnumber_ilp.rs @@ -40,8 +40,6 @@ impl ReductionResult for ReductionDomaticNumberToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.n; let mut config = vec![0; n]; @@ -124,7 +122,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/maximumedgeweightedkclique_ilp.rs b/src/rules/maximumedgeweightedkclique_ilp.rs index c7909601b..e55ba9fda 100644 --- a/src/rules/maximumedgeweightedkclique_ilp.rs +++ b/src/rules/maximumedgeweightedkclique_ilp.rs @@ -62,8 +62,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) @@ -182,7 +180,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]).unwrap(), vec![5, 4, -1, 1, 0], 3, ) @@ -194,7 +192,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]).unwrap(), vec![5.0, 4.0, -1.0, 1.0, 0.0], 3, ) diff --git a/src/rules/maximumindependentset_casts.rs b/src/rules/maximumindependentset_casts.rs index ff7d07906..968cd0458 100644 --- a/src/rules/maximumindependentset_casts.rs +++ b/src/rules/maximumindependentset_casts.rs @@ -19,7 +19,7 @@ impl_variant_reduction!( MaximumIndependentSet, >, )?, - src.weights().to_vec()) + src.weights().to_vec()).map_err(crate::rules::ReductionError::construction::, MaximumIndependentSet>)? ); impl_variant_reduction!( @@ -34,7 +34,7 @@ impl_variant_reduction!( MaximumIndependentSet, >, )?, - src.weights().to_vec()) + src.weights().to_vec()).map_err(crate::rules::ReductionError::construction::, MaximumIndependentSet>)? ); impl_variant_reduction!( @@ -43,8 +43,8 @@ impl_variant_reduction!( fields: [num_vertices, num_edges], aggregate: identity, |src| MaximumIndependentSet::new( - SimpleGraph::new(src.num_vertices(), Graph::edges(src.graph())), - src.weights().to_vec()) + SimpleGraph::new(src.num_vertices(), Graph::edges(src.graph())).map_err(crate::rules::ReductionError::construction::, MaximumIndependentSet>)?, + src.weights().to_vec()).map_err(crate::rules::ReductionError::construction::, MaximumIndependentSet>)? ); // Graph representation reductions with unit weights @@ -60,7 +60,7 @@ impl_variant_reduction!( MaximumIndependentSet, >, )?, - src.weights().to_vec()) + src.weights().to_vec()).map_err(crate::rules::ReductionError::construction::, MaximumIndependentSet>)? ); impl_variant_reduction!( @@ -69,8 +69,8 @@ impl_variant_reduction!( fields: [num_vertices, num_edges], aggregate: identity, |src| MaximumIndependentSet::new( - SimpleGraph::new(src.num_vertices(), Graph::edges(src.graph())), - src.weights().to_vec()) + SimpleGraph::new(src.num_vertices(), Graph::edges(src.graph())).map_err(crate::rules::ReductionError::construction::, MaximumIndependentSet>)?, + src.weights().to_vec()).map_err(crate::rules::ReductionError::construction::, MaximumIndependentSet>)? ); // Unit-to-integer weight reductions @@ -80,7 +80,7 @@ impl_variant_reduction!( fields: [num_vertices, num_edges], aggregate: identity, |src| MaximumIndependentSet::new( - src.graph().clone(), vec![1_i64; src.num_vertices()]) + src.graph().clone(), vec![1_i64; src.num_vertices()]).map_err(crate::rules::ReductionError::construction::, MaximumIndependentSet>)? ); #[cfg(test)] @@ -94,7 +94,8 @@ mod tests { let source = MaximumIndependentSet::new( KingsSubgraph::new(vec![(MAX_EXACT_F64_INTEGER + 1, 0)]), vec![1_i64], - ); + ) + .unwrap(); assert!(matches!( ReduceTo::>::reduce_to(&source), @@ -107,7 +108,8 @@ mod tests { let source = MaximumIndependentSet::new( TriangularSubgraph::new(vec![(MAX_EXACT_F64_INTEGER, 0), (MAX_EXACT_F64_INTEGER, 1)]), vec![1_i64, 1_i64], - ); + ) + .unwrap(); assert!(matches!( ReduceTo::>::reduce_to(&source), @@ -122,7 +124,7 @@ impl_variant_reduction!( fields: [num_vertices, num_edges], aggregate: identity, |src| MaximumIndependentSet::new( - src.graph().clone(), vec![1_i64; src.num_vertices()]) + src.graph().clone(), vec![1_i64; src.num_vertices()]).map_err(crate::rules::ReductionError::construction::, MaximumIndependentSet>)? ); impl_variant_reduction!( @@ -131,5 +133,5 @@ impl_variant_reduction!( fields: [num_vertices, num_edges], aggregate: identity, |src| MaximumIndependentSet::new( - src.graph().clone(), vec![1_i64; src.num_vertices()]) + src.graph().clone(), vec![1_i64; src.num_vertices()]).map_err(crate::rules::ReductionError::construction::, MaximumIndependentSet>)? ); diff --git a/src/rules/maximumindependentset_gridgraph.rs b/src/rules/maximumindependentset_gridgraph.rs index 1542a67fb..58a335e3c 100644 --- a/src/rules/maximumindependentset_gridgraph.rs +++ b/src/rules/maximumindependentset_gridgraph.rs @@ -29,8 +29,6 @@ impl ReductionResult for ReductionISSimpleOneToGridOne { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let encoded = crate::config::bits_to_config(target_solution); let mapped = self.mapping_result.map_config_back(&encoded)?; Ok(crate::config::config_to_bits(&mapped)) @@ -56,7 +54,9 @@ impl ReduceTo> })?; let grid = result.to_kings_subgraph(); let weights = vec![One; grid.num_vertices()]; - let target = MaximumIndependentSet::new(grid, weights); + let target = MaximumIndependentSet::new(grid, weights).map_err( + >>::target_construction, + )?; Ok(ReductionISSimpleOneToGridOne { target, mapping_result: result, diff --git a/src/rules/maximumindependentset_maximumclique.rs b/src/rules/maximumindependentset_maximumclique.rs index 98adc9190..f91cb1498 100644 --- a/src/rules/maximumindependentset_maximumclique.rs +++ b/src/rules/maximumindependentset_maximumclique.rs @@ -32,21 +32,19 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } fn reduce_is_to_clique( src: &MaximumIndependentSet, -) -> ReductionISToClique { +) -> Result, crate::registry::ConstructionError> { let comp_edges = super::graph_helpers::complement_edges(src.graph()); let target = MaximumClique::new( - SimpleGraph::new(src.graph().num_vertices(), comp_edges), + SimpleGraph::new(src.graph().num_vertices(), comp_edges)?, src.weights().to_vec(), - ); - ReductionISToClique { target } + )?; + Ok(ReductionISToClique { target }) } #[reduction( @@ -59,7 +57,8 @@ impl ReduceTo> for MaximumIndependentSet; fn reduce_to(&self) -> Result { - Ok(reduce_is_to_clique(self)) + reduce_is_to_clique(self) + .map_err(>>::target_construction) } } @@ -73,7 +72,8 @@ impl ReduceTo> for MaximumIndependentSet; fn reduce_to(&self) -> Result { - Ok(reduce_is_to_clique(self)) + reduce_is_to_clique(self) + .map_err(>>::target_construction) } } @@ -86,9 +86,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, @@ -105,9 +106,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, diff --git a/src/rules/maximumindependentset_maximumsetpacking.rs b/src/rules/maximumindependentset_maximumsetpacking.rs index 6695e87f4..f8300671f 100644 --- a/src/rules/maximumindependentset_maximumsetpacking.rs +++ b/src/rules/maximumindependentset_maximumsetpacking.rs @@ -33,8 +33,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -95,8 +93,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -124,9 +120,12 @@ macro_rules! impl_sp_to_is { } let target = MaximumIndependentSet::new( - SimpleGraph::new(n, edges), + SimpleGraph::new(n, edges).map_err(>>::target_construction)?, self.weights_ref().clone(), - ); + ) + .map_err( + >>::target_construction, + )?; Ok(ReductionSPToIS { target }) } @@ -146,7 +145,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { @@ -164,7 +165,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/maximumindependentset_triangular.rs b/src/rules/maximumindependentset_triangular.rs index 644c75bae..55a5c8cb4 100644 --- a/src/rules/maximumindependentset_triangular.rs +++ b/src/rules/maximumindependentset_triangular.rs @@ -31,8 +31,6 @@ impl ReductionResult for ReductionISSimpleToTriangular { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let encoded = crate::config::bits_to_config(target_solution); let mapped = triangular::map_config_back(&self.mapping_result, &encoded)?; Ok(crate::config::config_to_bits(&mapped)) @@ -59,7 +57,9 @@ impl ReduceTo> let result = triangular::map_weighted(n, &edges).map_err(&mapping_error)?; let weights = triangular::map_unit_weights(&result).map_err(mapping_error)?; let grid = result.to_triangular_subgraph(); - let target = MaximumIndependentSet::new(grid, weights); + let target = MaximumIndependentSet::new(grid, weights).map_err( + >>::target_construction, + )?; Ok(ReductionISSimpleToTriangular { target, mapping_result: result, diff --git a/src/rules/maximumleafspanningtree_ilp.rs b/src/rules/maximumleafspanningtree_ilp.rs index 2623018b7..eeafb9a3f 100644 --- a/src/rules/maximumleafspanningtree_ilp.rs +++ b/src/rules/maximumleafspanningtree_ilp.rs @@ -43,8 +43,6 @@ impl ReductionResult for ReductionMaximumLeafSpanningTreeToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // First m variables are edge selectors target_solution[..self.num_edges] @@ -171,10 +169,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/maximumlikelihoodranking_ilp.rs b/src/rules/maximumlikelihoodranking_ilp.rs index 954b09627..3fec79608 100644 --- a/src/rules/maximumlikelihoodranking_ilp.rs +++ b/src/rules/maximumlikelihoodranking_ilp.rs @@ -43,8 +43,6 @@ impl ReductionResult for ReductionMaximumLikelihoodRankingToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.n; if n == 0 { @@ -141,7 +139,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -93,7 +91,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/maximummatching_maximumsetpacking.rs b/src/rules/maximummatching_maximumsetpacking.rs index 1d98ef7a1..2f17323d6 100644 --- a/src/rules/maximummatching_maximumsetpacking.rs +++ b/src/rules/maximummatching_maximumsetpacking.rs @@ -34,8 +34,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -81,7 +79,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/maximumsetpacking_ilp.rs b/src/rules/maximumsetpacking_ilp.rs index 82766a60e..d600aef91 100644 --- a/src/rules/maximumsetpacking_ilp.rs +++ b/src/rules/maximumsetpacking_ilp.rs @@ -33,14 +33,12 @@ impl ReductionResult for ReductionSPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } #[reduction( - transform = exact { + transform = upper_bound { num_vars = "num_sets", num_constraints = "universe_size", }, diff --git a/src/rules/maximumsetpacking_qubo.rs b/src/rules/maximumsetpacking_qubo.rs index 2a7559884..24028e8fd 100644 --- a/src/rules/maximumsetpacking_qubo.rs +++ b/src/rules/maximumsetpacking_qubo.rs @@ -34,8 +34,6 @@ impl ReductionResult for ReductionSPToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -62,21 +60,21 @@ impl ReduceTo> for MaximumSetPacking { >("computing the set-packing conflict penalty")); } - let mut matrix = vec![vec![0.0; n]; n]; + let mut matrix = vec![std::collections::BTreeMap::new(); n]; // Diagonal: -w_i for i in 0..n { - matrix[i][i] = -weights[i]; + matrix[i].insert(i, -weights[i]); } // Off-diagonal: P for overlapping pairs for (i, j) in self.overlapping_pairs() { let (a, b) = if i < j { (i, j) } else { (j, i) }; - matrix[a][b] += penalty; + *matrix[a].entry(b).or_insert(0.0) += penalty; } Ok(ReductionSPToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { + target: QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::, QUBO>( message, ) diff --git a/src/rules/minimumcapacitatedspanningtree_ilp.rs b/src/rules/minimumcapacitatedspanningtree_ilp.rs index 0886ad247..ebd03d4a3 100644 --- a/src/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/rules/minimumcapacitatedspanningtree_ilp.rs @@ -50,8 +50,6 @@ impl ReductionResult for ReductionMinimumCapacitatedSpanningTreeToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // First m variables are edge selectors target_solution[..self.num_edges] @@ -224,12 +222,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs b/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs index 0d6df7ddb..1e22293d1 100644 --- a/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs +++ b/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs @@ -47,8 +47,6 @@ impl ReductionResult for ReductionMCMFToMCC { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_original_arcs].to_vec()) } } @@ -118,7 +116,13 @@ impl ReduceTo for MinimumCostMaximumFlow { >("negating the return-arc cost") })?); - let target = MinimumCostCirculation::new(DirectedGraph::new(n, arcs), capacities, costs); + let target = MinimumCostCirculation::new( + DirectedGraph::new(n, arcs) + .map_err(>::target_construction)?, + capacities, + costs, + ) + .map_err(>::target_construction)?; Ok(ReductionMCMFToMCC { target, @@ -139,7 +143,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec 0) with // flow value 3, giving config [2, 1, 1, 1, 2, 3]. let source = MinimumCostMaximumFlow::new( - DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]), + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]).unwrap(), 0, 3, vec![2, 1, 1, 1, 2], diff --git a/src/rules/minimumcoveringbycliques_ilp.rs b/src/rules/minimumcoveringbycliques_ilp.rs index fc139660b..ecd5ad80f 100644 --- a/src/rules/minimumcoveringbycliques_ilp.rs +++ b/src/rules/minimumcoveringbycliques_ilp.rs @@ -43,21 +43,15 @@ impl ReductionResult for ReductionMinimumCoveringByCliquesToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - (0..self.num_edges) - .map(|edge| { + Ok((0..self.num_edges) + .flat_map(|edge| { (0..self.num_edges) - .find(|&clique| { + .filter(move |&clique| { target_solution[self.y_offset + edge * self.num_edges + clique] == 1 }) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "edge {edge} is not covered by any clique" - )) - }) + .take(1) }) - .collect() + .collect()) } } @@ -150,10 +144,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs b/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs index 892f31b03..5af50195e 100644 --- a/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs +++ b/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs @@ -8,7 +8,6 @@ use crate::models::graph::{MinimumCoveringByCliques, MinimumIntersectionGraphBas use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::Problem; use std::collections::BTreeMap; #[derive(Debug, Clone)] @@ -16,37 +15,20 @@ pub struct ReductionMinimumCoveringByCliquesToMinimumIntersectionGraphBasis { target: MinimumIntersectionGraphBasis, } -fn extract_edge_clique_cover( - graph: &SimpleGraph, - target_solution: &[Vec], -) -> Option> { - let n = graph.num_vertices(); +fn extract_edge_clique_cover(graph: &SimpleGraph, target_solution: &[Vec]) -> Vec { let m = graph.num_edges(); - - if target_solution.len() != n || target_solution.iter().any(|row| row.len() != m) { - return None; - } - - if m == 0 { - return Some(Vec::new()); - } - let mut label_map = BTreeMap::new(); - let mut next_label = 0usize; let mut source_solution = Vec::with_capacity(m); - for (u, v) in graph.edges() { - let shared_label = - (0..m).find(|&slot| target_solution[u][slot] && target_solution[v][slot])?; - let compressed = *label_map.entry(shared_label).or_insert_with(|| { - let label = next_label; - next_label += 1; - label - }); - source_solution.push(compressed); + for shared_label in (0..m) + .filter(|&slot| target_solution[u][slot] && target_solution[v][slot]) + .take(1) + { + let next_label = label_map.len(); + source_solution.push(*label_map.entry(shared_label).or_insert(next_label)); + } } - - Some(source_solution) + source_solution } #[cfg(any(test, feature = "example-db"))] @@ -86,21 +68,10 @@ impl ReductionResult for ReductionMinimumCoveringByCliquesToMinimumIntersectionG &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - Ok({ - if !self.target.evaluate(target_solution)?.is_valid() { - return Err(crate::rules::ExtractionError::invalid( - "target configuration is not a valid intersection graph basis", - )); - } - - extract_edge_clique_cover(self.target.graph(), target_solution).ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "target basis does not assign a shared label to every source edge", - ) - })? - }) + Ok(extract_edge_clique_cover( + self.target.graph(), + target_solution, + )) } } @@ -129,10 +100,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) @@ -106,12 +104,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs b/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs index a00026804..198214b0d 100644 --- a/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs +++ b/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs @@ -29,6 +29,8 @@ pub struct ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { target: QUBO, block_offsets: Vec, block_sizes: Vec, + omitted_constant: f64, + feasible_energy_upper: f64, } impl ReductionResult for ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { @@ -39,36 +41,47 @@ impl ReductionResult for ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { &self.target } + /// Decode a qualifying optimum after the energy relation establishes source + /// feasibility. Such an optimum is one-hot and obeys every allowed pair. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - self.block_offsets + Ok(self + .block_offsets .iter() .zip(&self.block_sizes) - .enumerate() - .map(|(link, (&start, &size))| { - let mut selected = target_solution[start..start + size] + .map(|(&start, &size)| { + target_solution[start..start + size] .iter() - .enumerate() - .filter_map(|(orientation, &bit)| bit.then_some(orientation)); - match (selected.next(), selected.next()) { - (Some(orientation), None) => Ok(orientation), - (None, _) => Err(crate::rules::ExtractionError::invalid(format!( - "link {link} has no selected orientation" - ))), - (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( - "link {link} has multiple selected orientations" - ))), - } + .position(|&bit| bit) + .unwrap() }) - .collect() + .collect()) } } -#[reduction(transform = exact { +impl crate::rules::AggregateReductionResult + for ReductionMinimumDiscretePlanarInverseKinematicsToQUBO +{ + type Source = MinimumDiscretePlanarInverseKinematics; + type Target = QUBO; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, value: crate::types::Min) -> crate::types::Min { + crate::types::Min( + value + .0 + .filter(|&energy| energy < self.feasible_energy_upper) + .map(|energy| energy + self.omitted_constant), + ) + } +} + +#[reduction(aggregate = custom, transform = exact { num_vars = "num_orientation_samples", })] impl ReduceTo> for MinimumDiscretePlanarInverseKinematics { @@ -95,12 +108,25 @@ impl ReduceTo> for MinimumDiscretePlanarInverseKinematics { // instance is one-hot and pair-feasible. let sum_abs_x: f64 = x_coeffs.iter().map(|coeff| coeff.abs()).sum(); let sum_abs_y: f64 = y_coeffs.iter().map(|coeff| coeff.abs()).sum(); - let penalty = 1.0 + (sum_abs_x + gx.abs()).powi(2) + (sum_abs_y + gy.abs()).powi(2); + let distance_bound = (sum_abs_x + gx.abs()).powi(2) + (sum_abs_y + gy.abs()).powi(2); + // Leave a gap proportional to the scale, rather than adding one to a + // large floating-point number that may round back to itself. + let penalty = 2.0 * (1.0 + distance_bound); + let omitted_constant = gx * gx + gy * gy + penalty * block_sizes.len() as f64; + let feasible_energy_upper = 1.5 * (1.0 + distance_bound) - omitted_constant; + if !omitted_constant.is_finite() || !feasible_energy_upper.is_finite() { + return Err(crate::rules::ReductionError::non_finite_result::< + Self, + QUBO, + >( + "computing the inverse-kinematics energy relation" + )); + } - let mut matrix = vec![vec![0.0; total_vars]; total_vars]; + let mut matrix = vec![std::collections::BTreeMap::new(); total_vars]; let mut add_upper = |i: usize, j: usize, value: f64| { let (lo, hi) = if i <= j { (i, j) } else { (j, i) }; - matrix[lo][hi] += value; + *matrix[lo].entry(hi).or_insert(0.0) += value; }; // Position objective: (X - g_x)^2 + (Y - g_y)^2, dropping the @@ -156,14 +182,12 @@ impl ReduceTo> for MinimumDiscretePlanarInverseKinematics { } Ok(ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { - crate::rules::ReductionError::construction::< - MinimumDiscretePlanarInverseKinematics, - QUBO, - >(message) - })?, + target: QUBO::from_rows(matrix) + .map_err(>>::target_construction)?, block_offsets, block_sizes, + omitted_constant, + feasible_energy_upper, }) } } diff --git a/src/rules/minimumdominatingset_ilp.rs b/src/rules/minimumdominatingset_ilp.rs index 5b1d3f945..302004802 100644 --- a/src/rules/minimumdominatingset_ilp.rs +++ b/src/rules/minimumdominatingset_ilp.rs @@ -40,8 +40,6 @@ impl ReductionResult for ReductionDSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -95,7 +93,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/minimumedgecostflow_ilp.rs b/src/rules/minimumedgecostflow_ilp.rs index dd30eb04a..aa099f79c 100644 --- a/src/rules/minimumedgecostflow_ilp.rs +++ b/src/rules/minimumedgecostflow_ilp.rs @@ -47,8 +47,6 @@ impl ReductionResult for ReductionMECFToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(&target_solution[..self.num_edges]) } } @@ -140,13 +138,15 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/minimumexternalmacrodatacompression_ilp.rs b/src/rules/minimumexternalmacrodatacompression_ilp.rs index ba34bba53..5b517e204 100644 --- a/src/rules/minimumexternalmacrodatacompression_ilp.rs +++ b/src/rules/minimumexternalmacrodatacompression_ilp.rs @@ -125,8 +125,6 @@ impl ReductionResult for ReductionEMDCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.layout.n; let k = self.alphabet_size; @@ -135,27 +133,10 @@ impl ReductionResult for ReductionEMDCToILP { // Build D-slots let mut d_slots = vec![empty; n]; for j in 0..n { - let symbols: Vec<_> = (0..k) - .filter(|&c| target_solution[self.layout.d_var(j, c)] == 1) - .collect(); if target_solution[self.layout.d_used_var(j)] == 1 { - match symbols.as_slice() { - [symbol] => d_slots[j] = *symbol, - [] => { - return Err(crate::rules::ExtractionError::invalid(format!( - "dictionary slot {j} is active without a symbol" - ))) - } - _ => { - return Err(crate::rules::ExtractionError::invalid(format!( - "dictionary slot {j} selects multiple symbols" - ))) - } - } - } else if !symbols.is_empty() { - return Err(crate::rules::ExtractionError::invalid(format!( - "inactive dictionary slot {j} selects a symbol" - ))); + d_slots[j] = (0..k) + .filter(|&c| target_solution[self.layout.d_var(j, c)] == 1) + .sum(); } } @@ -173,23 +154,14 @@ impl ReductionResult for ReductionEMDCToILP { }) .collect(); if target_solution[self.layout.lit_var(pos)] == 1 { - if !pointers.is_empty() { - return Err(crate::rules::ExtractionError::invalid(format!( - "position {pos} selects both a literal and a pointer" - ))); - } // Literal at position pos c_slots[c_pos] = self.source_string[pos]; c_pos += 1; pos += 1; continue; } - let [(d_start, length)] = pointers.as_slice() else { - return Err(crate::rules::ExtractionError::invalid(format!( - "position {pos} must select exactly one pointer" - ))); - }; - let ptr_idx = encode_pointer(n, *d_start, *length); + let (d_start, length) = pointers[0]; + let ptr_idx = encode_pointer(n, d_start, length); c_slots[c_pos] = k + 1 + ptr_idx; c_pos += 1; pos += length; @@ -393,7 +365,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source).expect("reduction should succeed"); let layout = &reduction.layout; diff --git a/src/rules/minimumfaultdetectiontestset_ilp.rs b/src/rules/minimumfaultdetectiontestset_ilp.rs index bf0893c70..16082ec7c 100644 --- a/src/rules/minimumfaultdetectiontestset_ilp.rs +++ b/src/rules/minimumfaultdetectiontestset_ilp.rs @@ -31,8 +31,6 @@ impl ReductionResult for ReductionMFDTSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok((0..self.num_inputs) .map(|input| { (0..self.num_outputs) @@ -151,7 +149,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/minimumfeedbackarcset_ilp.rs b/src/rules/minimumfeedbackarcset_ilp.rs index 2de455d85..374b5fabc 100644 --- a/src/rules/minimumfeedbackarcset_ilp.rs +++ b/src/rules/minimumfeedbackarcset_ilp.rs @@ -45,8 +45,6 @@ impl ReductionResult for ReductionFASToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_arcs] .iter() .map(|&value| value == 1) @@ -130,8 +128,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec0): source_config = [0, 0, 1] // ILP solution: y_0=0, y_1=0, y_2=1, o_0=0, o_1=1, o_2=2 - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let source = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let source = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]).unwrap(); crate::example_db::specs::rule_example_via_ilp::<_, i64>(source) }, }] diff --git a/src/rules/minimumfeedbackvertexset_ilp.rs b/src/rules/minimumfeedbackvertexset_ilp.rs index 8b712b1a1..49a721c39 100644 --- a/src/rules/minimumfeedbackvertexset_ilp.rs +++ b/src/rules/minimumfeedbackvertexset_ilp.rs @@ -42,8 +42,6 @@ impl ReductionResult for ReductionMFVSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) @@ -124,8 +122,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec 1 -> 2 -> 0 (FVS = 1 vertex) - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let source = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let source = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]).unwrap(); crate::example_db::specs::rule_example_via_ilp::<_, i64>(source) }, }] diff --git a/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs b/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs index 1e336e56b..ff1a2e987 100644 --- a/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs +++ b/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs @@ -34,13 +34,6 @@ impl ReductionResult for ReductionFVSToCodeGen { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if value.0.is_none() { - return Err(crate::rules::ExtractionError::invalid( - "target order must be a permutation respecting expression dependencies", - )); - } Ok(self .chain_start .iter() @@ -105,7 +98,10 @@ impl ReduceTo for MinimumFeedbackVertex } debug_assert_eq!(next_internal, num_vertices); let target = - MinimumCodeGenerationUnlimitedRegisters::new(num_vertices, left_arcs, right_arcs); + MinimumCodeGenerationUnlimitedRegisters::new(num_vertices, left_arcs, right_arcs) + .map_err( + >::target_construction, + )?; Ok(ReductionFVSToCodeGen { target, chain_start, @@ -118,9 +114,10 @@ impl ReduceTo for MinimumFeedbackVertex fn issue_example_source() -> MinimumFeedbackVertexSet { use crate::topology::DirectedGraph; MinimumFeedbackVertexSet::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(), vec![One; 3], ) + .unwrap() } #[cfg(feature = "example-db")] diff --git a/src/rules/minimumgraphbandwidth_ilp.rs b/src/rules/minimumgraphbandwidth_ilp.rs index 76e1b5692..936cb67e7 100644 --- a/src/rules/minimumgraphbandwidth_ilp.rs +++ b/src/rules/minimumgraphbandwidth_ilp.rs @@ -38,14 +38,12 @@ impl ReductionResult for ReductionMGBToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_vertices, self.num_vertices, 0, - ) + )) } } @@ -147,8 +145,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/minimumhittingset_ilp.rs b/src/rules/minimumhittingset_ilp.rs index fe8f498d9..5eb9d75f2 100644 --- a/src/rules/minimumhittingset_ilp.rs +++ b/src/rules/minimumhittingset_ilp.rs @@ -25,8 +25,6 @@ impl ReductionResult for ReductionHSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -65,7 +63,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/minimuminternalmacrodatacompression_ilp.rs b/src/rules/minimuminternalmacrodatacompression_ilp.rs index 0ff986409..820b5c0fc 100644 --- a/src/rules/minimuminternalmacrodatacompression_ilp.rs +++ b/src/rules/minimuminternalmacrodatacompression_ilp.rs @@ -99,8 +99,6 @@ impl ReductionResult for ReductionIMDCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.layout.n; let k = self.alphabet_size; @@ -291,7 +289,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source).expect("reduction should succeed"); let layout = &reduction.layout; diff --git a/src/rules/minimummatrixcover_ilp.rs b/src/rules/minimummatrixcover_ilp.rs index d279e1b67..c39aadd19 100644 --- a/src/rules/minimummatrixcover_ilp.rs +++ b/src/rules/minimummatrixcover_ilp.rs @@ -32,8 +32,6 @@ impl ReductionResult for ReductionMinimumMatrixCoverToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // First n variables are the sign variables x_0,...,x_{n-1} target_solution[..self.n] @@ -160,7 +158,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -110,10 +108,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/minimummaximalmatching_maximumachromaticnumber.rs b/src/rules/minimummaximalmatching_maximumachromaticnumber.rs index 65615aa27..4c43f65f2 100644 --- a/src/rules/minimummaximalmatching_maximumachromaticnumber.rs +++ b/src/rules/minimummaximalmatching_maximumachromaticnumber.rs @@ -46,8 +46,6 @@ impl ReductionResult for ReductionMMMToAchromatic { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ self.source_edges .iter() @@ -86,7 +84,9 @@ impl ReduceTo> for MinimumMaximalMatching>>::target_construction, + )?); Ok(ReductionMMMToAchromatic { target, @@ -146,11 +146,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec color 0 // v3 (idx 4) -> color 2 // target_config = [1, 0, 3, 0, 2]; psi(H) = |V| - mm(G) = 4. - let source = MinimumMaximalMatching::new(BipartiteGraph::new( - 3, - 2, - vec![(0, 0), (1, 0), (1, 1), (2, 0)], - )); + let source = MinimumMaximalMatching::new( + BipartiteGraph::new(3, 2, vec![(0, 0), (1, 0), (1, 1), (2, 0)]).unwrap(), + ); crate::example_db::specs::rule_example_with_witness::< _, MaximumAchromaticNumber, diff --git a/src/rules/minimummaximalmatching_minimummatrixdomination.rs b/src/rules/minimummaximalmatching_minimummatrixdomination.rs index 03b0c98d7..556d6401b 100644 --- a/src/rules/minimummaximalmatching_minimummatrixdomination.rs +++ b/src/rules/minimummaximalmatching_minimummatrixdomination.rs @@ -80,7 +80,6 @@ impl ReductionResult for ReductionMMMToMatrixDomination { /// /// - **Drop:** if every edge of `B` incident to `u` is already dominated /// by `D \ {e1}`, set `D := D \ {e1}` (size strictly decreases). - /// Symmetric for `w` and `e2`. /// - **Swap:** otherwise, some edge `(u, x)` of `B` is currently dominated /// only by `e1`. This `x` must lie outside `V(D \ {e1})` and is /// therefore distinct from `w`, so `(u, x)` is not adjacent to `e2`. @@ -89,16 +88,14 @@ impl ReductionResult for ReductionMMMToMatrixDomination { /// /// Each iteration strictly decreases either `|D|` or the number of /// adjacent pairs, so the loop terminates in `O(|F|^2)` iterations. Each - /// iteration scans `O(|F|)` edges to find an adjacent pair, an EDS check, - /// and a swap candidate, for a total of `O(|F|^3)` time. The result is a + /// iteration scans `O(|F|)` edges to find an adjacent pair and an + /// undominated edge, for a total of `O(|F|^3)` time. The result is a /// matching that is an EDS, i.e. an independent EDS, which is precisely a /// maximal matching. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let graph = self.source.graph(); let edges = graph.edges(); @@ -124,87 +121,28 @@ impl ReductionResult for ReductionMMMToMatrixDomination { .collect(); let mut d: Vec = target_solution .iter() - .zip(target_ones.iter()) - .filter_map(|(&sel, &cell)| { - if sel { - Some(cell_to_source_edge.get(&cell).copied().ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "selected matrix cell {cell:?} has no source edge" - )) - })) - } else { - None - } - }) - .collect::>()?; - - // Step 2: Yannakakis-Gavril EDS -> independent EDS (maximal matching). - // Loop invariants: `d` is an EDS of the source graph; each iteration - // strictly decreases either |d| or the number of (unordered) pairs of - // adjacent edges inside `d`. - loop { - // Find an adjacent pair (e1_idx, e2_idx) inside `d`, sharing vertex v. - let pair = find_adjacent_pair(&d, &edges); - let Some((e1_idx, e2_idx, _shared)) = pair else { - break; // `d` is a matching; we are done. - }; - - // Try dropping e1_idx or e2_idx if the remainder is still an EDS. - let mut without_e1 = d.clone(); - let e1_position = d.iter().position(|&x| x == e1_idx).ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "edge-domination transformation lost its selected edge", - ) - })?; - without_e1.swap_remove(e1_position); - if is_edge_dominating_set(&without_e1, &edges) { - d = without_e1; - continue; - } - let mut without_e2 = d.clone(); - let e2_position = d.iter().position(|&x| x == e2_idx).ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "edge-domination transformation lost its selected edge", - ) - })?; - without_e2.swap_remove(e2_position); - if is_edge_dominating_set(&without_e2, &edges) { - d = without_e2; - continue; - } - - // Neither drop works -> perform a swap on one of e1 or e2. - // Choose endpoint not shared with the other edge: for e1=(u, v), - // e2=(v, w), the "non-shared" endpoint of e1 is u. - let (e1_a, e1_b) = edges[e1_idx]; - let (e2_a, e2_b) = edges[e2_idx]; - let shared = if e1_a == e2_a || e1_a == e2_b { - e1_a - } else { - e1_b - }; - let u = if e1_a == shared { e1_b } else { e1_a }; - let w = if e2_a == shared { e2_b } else { e2_a }; + .zip(target_ones) + .filter(|(selected, _)| **selected) + .map(|(_, cell)| cell_to_source_edge[cell]) + .collect(); - // Try to swap e1 := (u, x) where x ∉ V(d \ {e1}). The YG proof - // guarantees such x exists when neither drop succeeded. - if let Some(new_idx) = find_swap_edge(u, e1_idx, &d, &edges) { - d[e1_position] = new_idx; - continue; - } - // Symmetric swap on e2. - if let Some(new_idx) = find_swap_edge(w, e2_idx, &d, &edges) { - d[e2_position] = new_idx; - continue; + // Remove one of two adjacent selected edges. Any edge no longer + // dominated is private to its non-shared endpoint; replacing the + // removed edge with it preserves domination and reduces adjacency. + while let Some(position) = find_adjacent_pair(&d, &edges) { + let mut remaining = d.clone(); + remaining.swap_remove(position); + let covered: std::collections::HashSet<_> = remaining + .iter() + .flat_map(|&edge| [edges[edge].0, edges[edge].1]) + .collect(); + match edges + .iter() + .position(|&(u, v)| !covered.contains(&u) && !covered.contains(&v)) + { + Some(edge) => d[position] = edge, + None => d = remaining, } - - // YG guarantees that for an EDS at least one of the four moves - // above succeeds. Reaching this point implies the input was not - // a valid EDS (i.e., not a feasible MMD witness on the constructed - // instance), which violates the reduction's precondition. - return Err(crate::rules::ExtractionError::invalid( - "target matrix entries do not encode an edge-dominating set", - )); } // Step 3: encode the matching as a binary configuration over source edges. @@ -217,81 +155,19 @@ impl ReductionResult for ReductionMMMToMatrixDomination { } } -/// Return `Some((i, j, v))` where `i`, `j` are indices in `d` of two edges that -/// share vertex `v`, or `None` if all edges in `d` are pairwise independent. -fn find_adjacent_pair(d: &[usize], edges: &[(usize, usize)]) -> Option<(usize, usize, usize)> { - for (a_pos, &i) in d.iter().enumerate() { - let (iu, iv) = edges[i]; - for &j in &d[a_pos + 1..] { - let (ju, jv) = edges[j]; - if iu == ju || iu == jv { - return Some((i, j, iu)); - } - if iv == ju || iv == jv { - return Some((i, j, iv)); +/// Find the position of a selected edge adjacent to another selected edge. +fn find_adjacent_pair(d: &[usize], edges: &[(usize, usize)]) -> Option { + let mut incident = std::collections::HashMap::new(); + for (position, &edge) in d.iter().enumerate() { + for vertex in [edges[edge].0, edges[edge].1] { + if let Some(previous) = incident.insert(vertex, position) { + return Some(previous); } } } None } -/// Check whether the edge set `d` (indices into `edges`) dominates every edge -/// of `edges`. An edge `f` is dominated iff `f ∈ d` or `f` shares an endpoint -/// with some edge in `d`. -fn is_edge_dominating_set(d: &[usize], edges: &[(usize, usize)]) -> bool { - // Vertex cover of the candidate EDS. - let mut covered_vertices: std::collections::HashSet = std::collections::HashSet::new(); - for &i in d { - let (u, v) = edges[i]; - covered_vertices.insert(u); - covered_vertices.insert(v); - } - edges.iter().enumerate().all(|(f_idx, (u, v))| { - d.contains(&f_idx) || covered_vertices.contains(u) || covered_vertices.contains(v) - }) -} - -/// Find an edge index in `edges` that is (i) incident to vertex `endpoint`, -/// (ii) different from `excluded_idx`, and (iii) whose other endpoint lies -/// outside `V(d \ {excluded_idx})`. -/// -/// This is the swap candidate `(u, x)` from the Yannakakis-Gavril argument -/// when the drop move is not available for `excluded_idx`. -fn find_swap_edge( - endpoint: usize, - excluded_idx: usize, - d: &[usize], - edges: &[(usize, usize)], -) -> Option { - // Vertex cover of d \ {excluded_idx}. - let mut other_cover: std::collections::HashSet = std::collections::HashSet::new(); - for &i in d { - if i == excluded_idx { - continue; - } - let (u, v) = edges[i]; - other_cover.insert(u); - other_cover.insert(v); - } - for (k, &(u, v)) in edges.iter().enumerate() { - if k == excluded_idx { - continue; - } - let (e_endpoint, other) = if u == endpoint { - (u, v) - } else if v == endpoint { - (v, u) - } else { - continue; - }; - debug_assert_eq!(e_endpoint, endpoint); - if !other_cover.contains(&other) { - return Some(k); - } - } - None -} - #[reduction( transform = exact { num_rows = "num_vertices", @@ -319,7 +195,8 @@ impl ReduceTo for MinimumMaximalMatching>::target_construction)?; Ok(ReductionMMMToMatrixDomination { target, @@ -361,11 +238,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( source, SolutionPair { diff --git a/src/rules/minimummetricdimension_ilp.rs b/src/rules/minimummetricdimension_ilp.rs index a10a1e9a5..37f9d32bd 100644 --- a/src/rules/minimummetricdimension_ilp.rs +++ b/src/rules/minimummetricdimension_ilp.rs @@ -42,8 +42,6 @@ impl ReductionResult for ReductionMDToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -95,7 +93,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/minimummultiwaycut_ilp.rs b/src/rules/minimummultiwaycut_ilp.rs index 51c157007..5c90107da 100644 --- a/src/rules/minimummultiwaycut_ilp.rs +++ b/src/rules/minimummultiwaycut_ilp.rs @@ -46,8 +46,6 @@ impl ReductionResult for ReductionMMCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let offset = self.k * self.n; (0..self.m) @@ -145,8 +143,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(problem) }, }] diff --git a/src/rules/minimummultiwaycut_qubo.rs b/src/rules/minimummultiwaycut_qubo.rs index 8679c2285..f60e71bd7 100644 --- a/src/rules/minimummultiwaycut_qubo.rs +++ b/src/rules/minimummultiwaycut_qubo.rs @@ -7,7 +7,8 @@ //! QUBO Hamiltonian: H = H_A + H_B //! //! H_A enforces valid partition (one-hot per vertex) and terminal pinning. -//! H_B encodes the cut cost objective. +//! H_B encodes nonnegative cut costs. Negative edges are always deleted during +//! extraction: deleting them improves the objective and preserves separation. //! //! Reference: Heidari, Dinneen & Delmas (2022). @@ -24,6 +25,7 @@ pub struct ReductionMinimumMultiwayCutToQUBO { num_vertices: usize, num_terminals: usize, edges: Vec<(usize, usize)>, + negative_edges: Vec, } impl ReductionResult for ReductionMinimumMultiwayCutToQUBO { @@ -34,38 +36,27 @@ impl ReductionResult for ReductionMinimumMultiwayCutToQUBO { &self.target } - /// Decode one-hot assignment: for each vertex find its terminal, then - /// for each edge check if endpoints are in different terminals. + /// Map an optimal target assignment to an optimal edge deletion set. + /// The penalty guarantees one-hot, terminal-pinned assignments at every + /// optimum. All source instances are feasible by deleting every edge. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - Ok({ - let k = self.num_terminals; - let n = self.num_vertices; - - // For each vertex, find which terminal position it is assigned to - let assignments: Vec = (0..n) - .map(|vertex| { - let mut selected = - (0..k).filter(|&terminal| target_solution[vertex * k + terminal]); - match (selected.next(), selected.next()) { - (Some(terminal), None) => Ok(terminal), - _ => Err(crate::rules::ExtractionError::invalid(format!( - "vertex {vertex} does not have exactly one terminal assignment" - ))), - } - }) - .collect::>()?; - - // For each edge, output 1 (cut) if endpoints differ, 0 (keep) otherwise - self.edges - .iter() - .map(|&(u, v)| assignments[u] != assignments[v]) - .collect() - }) + let k = self.num_terminals; + let assignments: Vec = (0..self.num_vertices) + .map(|vertex| { + (0..k) + .find(|&terminal| target_solution[vertex * k + terminal]) + .unwrap() + }) + .collect(); + Ok(self + .edges + .iter() + .zip(&self.negative_edges) + .map(|(&(u, v), &negative)| negative || assignments[u] != assignments[v]) + .collect()) } } @@ -91,26 +82,24 @@ impl ReduceTo> for MinimumMultiwayCut { .checked_mul(k) .ok_or_else(|| overflow("computing the number of QUBO variables"))?; - // Penalty: sum of all edge weights + 1 + // Nonnegative cut costs cannot reward invalid assignments. A feasible + // pinned partition costs at most their sum, below one penalty unit. let alpha = edge_weights.iter().try_fold(0i64, |total, &weight| { total - .checked_add( - weight - .checked_abs() - .ok_or_else(|| overflow("taking the absolute value of a cut weight"))?, - ) - .ok_or_else(|| overflow("summing absolute cut weights")) + .checked_add(weight.max(0)) + .ok_or_else(|| overflow("summing nonnegative cut weights")) })?; let alpha = alpha .checked_add(1) .ok_or_else(|| overflow("computing the partition penalty"))?; - let mut matrix = vec![vec![0i64; nq]; nq]; + let mut matrix = vec![std::collections::BTreeMap::new(); nq]; // Helper: add value to upper-triangular position let mut add_upper = |i: usize, j: usize, val: i64| { let (lo, hi) = if i <= j { (i, j) } else { (j, i) }; - matrix[lo][hi] = matrix[lo][hi] + let coefficient = matrix[lo].entry(hi).or_insert(0i64); + *coefficient = coefficient .checked_add(val) .ok_or_else(|| overflow("adding a multiway-cut QUBO coefficient"))?; Ok::<(), crate::rules::ReductionError>(()) @@ -158,7 +147,7 @@ impl ReduceTo> for MinimumMultiwayCut { // For each edge (u,v) with weight w, for each pair of distinct // terminal positions s != t: add w to Q[u*k+s, v*k+t] for (edge_idx, &(u, v)) in edges.iter().enumerate() { - let w = edge_weights[edge_idx]; + let w = edge_weights[edge_idx].max(0); for s in 0..k { for t in 0..k { if s != t { @@ -169,15 +158,12 @@ impl ReduceTo> for MinimumMultiwayCut { } Ok(ReductionMinimumMultiwayCutToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { - crate::rules::ReductionError::construction::< - MinimumMultiwayCut, - QUBO, - >(message) - })?, + target: QUBO::from_rows(matrix) + .map_err(>>::target_construction)?, num_vertices: n, num_terminals: k, edges, + negative_edges: edge_weights.iter().map(|&weight| weight < 0).collect(), }) } } @@ -192,8 +178,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/minimumsetcovering_ilp.rs b/src/rules/minimumsetcovering_ilp.rs index 6f208be75..e2b26d05f 100644 --- a/src/rules/minimumsetcovering_ilp.rs +++ b/src/rules/minimumsetcovering_ilp.rs @@ -37,8 +37,6 @@ impl ReductionResult for ReductionSCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -105,7 +103,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/minimumsummulticenter_ilp.rs b/src/rules/minimumsummulticenter_ilp.rs index bb6f82fd6..64b1a486e 100644 --- a/src/rules/minimumsummulticenter_ilp.rs +++ b/src/rules/minimumsummulticenter_ilp.rs @@ -45,8 +45,6 @@ impl ReductionResult for ReductionMSMCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) @@ -211,11 +209,12 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/minimumtardinesssequencing_ilp.rs b/src/rules/minimumtardinesssequencing_ilp.rs index e8533153d..a17c2bde1 100644 --- a/src/rules/minimumtardinesssequencing_ilp.rs +++ b/src/rules/minimumtardinesssequencing_ilp.rs @@ -30,12 +30,10 @@ impl ReductionResult for ReductionMTSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_tasks; - one_hot_decode(target_solution, n, n, 0)? + one_hot_decode(target_solution, n, n, 0) }) } } @@ -59,12 +57,10 @@ impl ReductionResult for ReductionMTSWeightedToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_tasks; - one_hot_decode(target_solution, n, n, 0)? + one_hot_decode(target_solution, n, n, 0) }) } } @@ -225,7 +221,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::new(3, vec![2, 3, 1], vec![(0, 2)]); + let source = + MinimumTardinessSequencing::::new(3, vec![2, 3, 1], vec![(0, 2)]).unwrap(); crate::example_db::specs::rule_example_via_ilp::<_, bool>(source) }, }, @@ -236,7 +233,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }, diff --git a/src/rules/minimumvertexcover_comparativecontainment.rs b/src/rules/minimumvertexcover_comparativecontainment.rs index 4e08fc68a..6dbc7fef3 100644 --- a/src/rules/minimumvertexcover_comparativecontainment.rs +++ b/src/rules/minimumvertexcover_comparativecontainment.rs @@ -34,13 +34,6 @@ impl ReductionResult for ReductionDecisionMVCToComparativeContainment { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - if !crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)? - .0 - { - return Err(crate::rules::ExtractionError::invalid( - "containment inequality is not satisfied", - )); - } Ok(target_solution.clone()) } } @@ -149,9 +142,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, diff --git a/src/rules/minimumvertexcover_ensemblecomputation.rs b/src/rules/minimumvertexcover_ensemblecomputation.rs index 710badf1f..a6fb0a4da 100644 --- a/src/rules/minimumvertexcover_ensemblecomputation.rs +++ b/src/rules/minimumvertexcover_ensemblecomputation.rs @@ -44,18 +44,10 @@ impl ReductionResult for ReductionVCToEC { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let crate::types::Min(Some(length)) = value else { - return Err(crate::rules::ExtractionError::invalid( - "target configuration does not encode a valid ensemble computation", - )); - }; - let meaningful_steps = usize::try_from(length).map_err(|_| { - crate::rules::ExtractionError::invalid( - "ensemble operation count cannot be represented as usize", - ) - })?; + let value = crate::traits::Problem::evaluate(self.target_problem(), target_solution)?; + // Evaluation supplies the meaningful prefix, which the mapping needs. + // The target witness premise already guarantees a feasible program. + let meaningful_steps = value.0.unwrap() as usize; let mut cover = vec![false; self.num_vertices]; let universe_size = self.target.universe_size(); for &[left, right] in target_solution @@ -133,7 +125,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let mut cover = vec![true; self.num_vertices]; for &symbol in target_solution { @@ -72,7 +70,8 @@ impl ReduceTo for MinimumVertexCover strings.push(edge_string); } - let target = LongestCommonSubsequence::new(num_vertices, strings); + let target = LongestCommonSubsequence::new(num_vertices, strings) + .map_err(>::target_construction)?; Ok(ReductionVCToLCS { target, num_vertices, @@ -94,9 +93,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( source, SolutionPair { diff --git a/src/rules/minimumvertexcover_maximumindependentset.rs b/src/rules/minimumvertexcover_maximumindependentset.rs index 5c43127d7..2e325b89b 100644 --- a/src/rules/minimumvertexcover_maximumindependentset.rs +++ b/src/rules/minimumvertexcover_maximumindependentset.rs @@ -31,8 +31,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&x| !x).collect()) } } @@ -48,9 +46,12 @@ impl ReduceTo> for MaximumIndependentSet Result { let target = MinimumVertexCover::new( - SimpleGraph::new(self.graph().num_vertices(), self.graph().edges()), + SimpleGraph::new(self.graph().num_vertices(), self.graph().edges()).map_err( + >>::target_construction, + )?, self.weights().to_vec(), - ); + ) + .map_err(>>::target_construction)?; Ok(ReductionISToVC { target }) } } @@ -77,8 +78,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&x| !x).collect()) } } @@ -94,9 +93,14 @@ impl ReduceTo> for MinimumVertexCover Result { let target = MaximumIndependentSet::new( - SimpleGraph::new(self.graph().num_vertices(), self.graph().edges()), + SimpleGraph::new(self.graph().num_vertices(), self.graph().edges()).map_err( + >>::target_construction, + )?, self.weights().to_vec(), - ); + ) + .map_err( + >>::target_construction, + )?; Ok(ReductionVCToIS { target }) } } @@ -107,12 +111,12 @@ pub(crate) fn canonical_rule_example_specs() -> Vec MinimumVertexCover { let (n, edges) = crate::topology::small_graphs::petersen(); - MinimumVertexCover::new(SimpleGraph::new(n, edges), vec![1i64; 10]) + MinimumVertexCover::new(SimpleGraph::new(n, edges).unwrap(), vec![1i64; 10]).unwrap() } fn mis_petersen() -> MaximumIndependentSet { let (n, edges) = crate::topology::small_graphs::petersen(); - MaximumIndependentSet::new(SimpleGraph::new(n, edges), vec![1i64; 10]) + MaximumIndependentSet::new(SimpleGraph::new(n, edges).unwrap(), vec![1i64; 10]).unwrap() } vec![ diff --git a/src/rules/minimumvertexcover_minimumfeedbackarcset.rs b/src/rules/minimumvertexcover_minimumfeedbackarcset.rs index ef8f39d10..d4ba576a5 100644 --- a/src/rules/minimumvertexcover_minimumfeedbackarcset.rs +++ b/src/rules/minimumvertexcover_minimumfeedbackarcset.rs @@ -35,8 +35,6 @@ impl ReductionResult for ReductionVCToFAS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_source_vertices].to_vec()) } } @@ -90,8 +88,10 @@ impl ReduceTo> for MinimumVertexCover>>::target_construction)?; + let target = MinimumFeedbackArcSet::new(graph, weights) + .map_err(>>::target_construction)?; Ok(ReductionVCToFAS { target, @@ -111,9 +111,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs b/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs index d5ed67e8a..605b1a6fd 100644 --- a/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs +++ b/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs @@ -30,8 +30,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -54,9 +52,11 @@ impl ReduceTo> for MinimumVertexCover>>::target_construction)?, self.weights().to_vec(), - ); + ) + .map_err(>>::target_construction)?; Ok(ReductionVCToFVS { target }) } @@ -82,9 +82,11 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, diff --git a/src/rules/minimumvertexcover_minimumhittingset.rs b/src/rules/minimumvertexcover_minimumhittingset.rs index 656cbcfd6..1d0fb043f 100644 --- a/src/rules/minimumvertexcover_minimumhittingset.rs +++ b/src/rules/minimumvertexcover_minimumhittingset.rs @@ -30,8 +30,6 @@ impl ReductionResult for ReductionVCToHS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -52,7 +50,8 @@ impl ReduceTo for MinimumVertexCover { // For each edge (u, v), create a 2-element subset {u, v}. let sets: Vec> = edges.iter().map(|&(u, v)| vec![u, v]).collect(); - let target = MinimumHittingSet::new(num_vertices, sets); + let target = MinimumHittingSet::new(num_vertices, sets) + .map_err(>::target_construction)?; Ok(ReductionVCToHS { target }) } @@ -79,9 +78,11 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( source, SolutionPair { diff --git a/src/rules/minimumvertexcover_minimummaximalmatching.rs b/src/rules/minimumvertexcover_minimummaximalmatching.rs index 06819a13b..542bafdb1 100644 --- a/src/rules/minimumvertexcover_minimummaximalmatching.rs +++ b/src/rules/minimumvertexcover_minimummaximalmatching.rs @@ -44,8 +44,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -66,7 +64,8 @@ impl ReduceTo> for MinimumVertexCover }) .collect(); - let target = MinimumSetCovering::with_weights(num_edges, sets, self.weights().to_vec()); + let target = MinimumSetCovering::with_weights(num_edges, sets, self.weights().to_vec()) + .map_err(>>::target_construction)?; Ok(ReductionVCToSC { target }) } @@ -80,7 +79,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/minimumvertexcover_minimumweightandorgraph.rs b/src/rules/minimumvertexcover_minimumweightandorgraph.rs index 247ce8733..4147bd6c2 100644 --- a/src/rules/minimumvertexcover_minimumweightandorgraph.rs +++ b/src/rules/minimumvertexcover_minimumweightandorgraph.rs @@ -27,8 +27,6 @@ impl ReductionResult for ReductionVCToAndOrGraph { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ (0..self.num_source_vertices) .map(|j| target_solution[self.sink_arc_start + j]) @@ -84,7 +82,8 @@ impl ReduceTo for MinimumVertexCover } let target = - MinimumWeightAndOrGraph::new(num_target_vertices, arcs, 0, gate_types, arc_weights); + MinimumWeightAndOrGraph::new(num_target_vertices, arcs, 0, gate_types, arc_weights) + .map_err(>::target_construction)?; Ok(ReductionVCToAndOrGraph { target, @@ -96,7 +95,11 @@ impl ReduceTo for MinimumVertexCover #[cfg(any(test, feature = "example-db"))] fn issue_example_source() -> MinimumVertexCover { - MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]) + MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap() } #[cfg(feature = "example-db")] diff --git a/src/rules/minimumweightdecoding_ilp.rs b/src/rules/minimumweightdecoding_ilp.rs index 970470c71..714b53710 100644 --- a/src/rules/minimumweightdecoding_ilp.rs +++ b/src/rules/minimumweightdecoding_ilp.rs @@ -44,8 +44,6 @@ impl ReductionResult for ReductionMinimumWeightDecodingToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_cols] .iter() .map(|&value| value == 1) @@ -116,7 +114,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/minmaxmulticenter_ilp.rs b/src/rules/minmaxmulticenter_ilp.rs index f4360ac63..08eaaaca5 100644 --- a/src/rules/minmaxmulticenter_ilp.rs +++ b/src/rules/minmaxmulticenter_ilp.rs @@ -49,8 +49,6 @@ impl ReductionResult for ReductionMMCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) @@ -249,11 +247,12 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/mixedchinesepostman_ilp.rs b/src/rules/mixedchinesepostman_ilp.rs index 88da8843a..33441eeec 100644 --- a/src/rules/mixedchinesepostman_ilp.rs +++ b/src/rules/mixedchinesepostman_ilp.rs @@ -30,8 +30,6 @@ impl ReductionResult for ReductionMCPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Return the orientation bits d_k in source edge order target_solution[..self.num_undirected_edges] @@ -390,7 +388,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec Vec Vec => , /// fields: [num_vertices, num_edges], /// |src| MaximumIndependentSet::new( -/// SimpleGraph::new(src.num_vertices(), Graph::edges(src.graph())), -/// src.weights()) +/// SimpleGraph::new(src.num_vertices(), Graph::edges(src.graph())).unwrap(), +/// src.weights()).unwrap() /// ); /// ``` #[macro_export] diff --git a/src/rules/monochromatictriangle_ilp.rs b/src/rules/monochromatictriangle_ilp.rs index 8a83cb1d6..0a26f6383 100644 --- a/src/rules/monochromatictriangle_ilp.rs +++ b/src/rules/monochromatictriangle_ilp.rs @@ -29,8 +29,6 @@ impl ReductionResult for ReductionMonochromaticTriangleToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -134,10 +132,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/multiplechoicebranching_ilp.rs b/src/rules/multiplechoicebranching_ilp.rs index cba6f76cd..93504b3d5 100644 --- a/src/rules/multiplechoicebranching_ilp.rs +++ b/src/rules/multiplechoicebranching_ilp.rs @@ -23,7 +23,6 @@ impl ReductionResult for ReductionMultipleChoiceBranchingToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; Ok(target_solution[..self.num_arcs] .iter() .map(|&selected| selected == 1) @@ -112,7 +111,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) @@ -158,10 +156,11 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/multiprocessorscheduling_ilp.rs b/src/rules/multiprocessorscheduling_ilp.rs index c3d8ed3cd..c727c31b3 100644 --- a/src/rules/multiprocessorscheduling_ilp.rs +++ b/src/rules/multiprocessorscheduling_ilp.rs @@ -37,14 +37,12 @@ impl ReductionResult for ReductionMSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_tasks, self.num_processors, 0, - ) + )) } } @@ -106,7 +104,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/naesatisfiability_ilp.rs b/src/rules/naesatisfiability_ilp.rs index 1c57fc3a5..2a91a142b 100644 --- a/src/rules/naesatisfiability_ilp.rs +++ b/src/rules/naesatisfiability_ilp.rs @@ -30,8 +30,6 @@ impl ReductionResult for ReductionNAESATToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/naesatisfiability_maxcut.rs b/src/rules/naesatisfiability_maxcut.rs index 55e8e81c7..7582b9652 100644 --- a/src/rules/naesatisfiability_maxcut.rs +++ b/src/rules/naesatisfiability_maxcut.rs @@ -42,14 +42,6 @@ impl ReductionResult for ReductionNAESATToMaxCut { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target cut does not certify a satisfying NAE assignment", - )); - } - Ok({ (0..self.source_num_vars) .map(|i| target_solution[2 * i]) @@ -200,7 +192,12 @@ impl ReduceTo> for NAESatisfiability { } Ok(ReductionNAESATToMaxCut { - target: MaxCut::new(SimpleGraph::new(total_vertices, edges), weights), + target: MaxCut::new( + SimpleGraph::new(total_vertices, edges) + .map_err(>>::target_construction)?, + weights, + ) + .map_err(>>::target_construction)?, source_num_vars: self.num_vars(), feasible_cut, }) diff --git a/src/rules/naesatisfiability_partitionintoperfectmatchings.rs b/src/rules/naesatisfiability_partitionintoperfectmatchings.rs index c7a09e983..ea46c02b9 100644 --- a/src/rules/naesatisfiability_partitionintoperfectmatchings.rs +++ b/src/rules/naesatisfiability_partitionintoperfectmatchings.rs @@ -69,8 +69,6 @@ impl ReductionResult for ReductionNAESATToPartitionIntoPerfectMatchings { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ self.layout .variables @@ -329,9 +327,14 @@ impl ReduceTo> for NAESatisfiability >(message.to_string()) })?; let target = PartitionIntoPerfectMatchings::new( - SimpleGraph::new(layout.num_vertices, layout.edges.clone()), + SimpleGraph::new(layout.num_vertices, layout.edges.clone()).map_err( + >>::target_construction, + )?, 2, - ); + ) + .map_err( + >>::target_construction, + )?; Ok(ReductionNAESATToPartitionIntoPerfectMatchings { target, layout }) } diff --git a/src/rules/naesatisfiability_setsplitting.rs b/src/rules/naesatisfiability_setsplitting.rs index 854e7ed69..284a9cac4 100644 --- a/src/rules/naesatisfiability_setsplitting.rs +++ b/src/rules/naesatisfiability_setsplitting.rs @@ -29,8 +29,6 @@ impl ReductionResult for ReductionNAESATToSetSplitting { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_source_variables].to_vec()) } } diff --git a/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs b/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs index 30729b498..9f8478b8c 100644 --- a/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs +++ b/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs @@ -8,14 +8,11 @@ use crate::models::misc::{Numerical3DimensionalMatching, NumericalMatchingWithTargetSums}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -use std::collections::BTreeMap; /// Result of reducing Numerical3DimensionalMatching to NumericalMatchingWithTargetSums. #[derive(Debug, Clone)] pub struct ReductionN3DMToNMTS { target: NumericalMatchingWithTargetSums, - source_sizes_w: Vec, - source_bound: i64, } impl ReductionResult for ReductionN3DMToNMTS { @@ -30,39 +27,23 @@ impl ReductionResult for ReductionN3DMToNMTS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ - let mut x_indices_by_pair_sum: BTreeMap> = BTreeMap::new(); - for (x_index, &y_index) in target_solution.iter().enumerate() { - let pair_sum = self.target.sizes_x()[x_index] - .checked_add(self.target.sizes_y()[y_index]) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "target pair sum overflows the target numeric domain", - ) - })?; - x_indices_by_pair_sum - .entry(pair_sum) - .or_default() - .push(x_index); - } + let mut pairs: Vec<_> = target_solution + .iter() + .enumerate() + .map(|(x, &y)| (self.target.sizes_x()[x] + self.target.sizes_y()[y], x, y)) + .collect(); + let mut targets: Vec<_> = self.target.targets().iter().copied().enumerate().collect(); + pairs.sort_unstable(); + targets.sort_unstable_by_key(|&(w, sum)| (sum, w)); - let mut x_perm = Vec::with_capacity(self.source_sizes_w.len()); - let mut y_perm = Vec::with_capacity(self.source_sizes_w.len()); - for &w_size in &self.source_sizes_w { - let target_sum = checked_target_sum(self.source_bound, w_size) - .map_err(crate::rules::ExtractionError::invalid)?; - let x_index = x_indices_by_pair_sum - .get_mut(&target_sum) - .and_then(Vec::pop) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "target matching does not realize required pair sum {target_sum}" - )) - })?; - x_perm.push(x_index); - y_perm.push(target_solution[x_index]); + // Feasibility equates the pair-sum and target multisets. Target + // index w retains the source W order from construction. + let mut x_perm = vec![0; targets.len()]; + let mut y_perm = vec![0; targets.len()]; + for ((w, _), (_, x, y)) in targets.into_iter().zip(pairs) { + x_perm[w] = x; + y_perm[w] = y; } x_perm.extend(y_perm); @@ -102,11 +83,7 @@ impl ReduceTo for Numerical3DimensionalMatching .map_err(map_error)?, ); - Ok(ReductionN3DMToNMTS { - target, - source_sizes_w: self.sizes_w().to_vec(), - source_bound: self.bound(), - }) + Ok(ReductionN3DMToNMTS { target }) } } diff --git a/src/rules/numericalmatchingwithtargetsums_ilp.rs b/src/rules/numericalmatchingwithtargetsums_ilp.rs index 3f658a8bf..a4cfe5092 100644 --- a/src/rules/numericalmatchingwithtargetsums_ilp.rs +++ b/src/rules/numericalmatchingwithtargetsums_ilp.rs @@ -48,8 +48,6 @@ impl ReductionResult for ReductionNMTSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let mut assignment = vec![0usize; self.m]; for (var_idx, triple) in self.triples.iter().enumerate() { diff --git a/src/rules/openshopscheduling_ilp.rs b/src/rules/openshopscheduling_ilp.rs index 41f385661..28c2416fc 100644 --- a/src/rules/openshopscheduling_ilp.rs +++ b/src/rules/openshopscheduling_ilp.rs @@ -91,7 +91,6 @@ impl ReductionResult for ReductionOSSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; let start = self.num_order_vars; let end = start + self.num_jobs * self.num_machines; crate::rules::ilp_helpers::decode_usize_values(&target_solution[start..end]) diff --git a/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs b/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs index 1efcd0b0b..46dde801c 100644 --- a/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs +++ b/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs @@ -30,13 +30,6 @@ impl ReductionResult for ReductionOptimalLinearArrangementToConsecutiveOnesMatri &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target column order is not a satisfying augmentation certificate", - )); - } // Validation establishes a permutation within the augmentation budget. // The NO sentinel has no such certificate; all remaining columns are // source vertices, including the empty permutation for an empty graph. @@ -112,10 +105,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::reduce_to(&source) diff --git a/src/rules/optimallineararrangement_ilp.rs b/src/rules/optimallineararrangement_ilp.rs index 934dd35b1..7a7dd13c3 100644 --- a/src/rules/optimallineararrangement_ilp.rs +++ b/src/rules/optimallineararrangement_ilp.rs @@ -38,14 +38,12 @@ impl ReductionResult for ReductionOLAToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_vertices, self.num_vertices, 0, - ) + )) } } @@ -148,8 +146,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs index e7a14a680..12fc0a640 100644 --- a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs +++ b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs @@ -36,8 +36,6 @@ impl ReductionResult for ReductionOLAToSequencingToMinimizeWeightedCompletionTim &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let mut arrangement = vec![0usize; self.num_vertices]; let mut next_position = 0usize; @@ -121,8 +119,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::reduce_to(&source) .expect("reduction should succeed"); diff --git a/src/rules/optimumcommunicationspanningtree_ilp.rs b/src/rules/optimumcommunicationspanningtree_ilp.rs index ee0c48bb7..5a47ed460 100644 --- a/src/rules/optimumcommunicationspanningtree_ilp.rs +++ b/src/rules/optimumcommunicationspanningtree_ilp.rs @@ -37,8 +37,6 @@ impl ReductionResult for ReductionOptimumCommunicationSpanningTreeToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_edges] .iter() .map(|&value| value == 1) @@ -177,7 +175,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/paintshop_ilp.rs b/src/rules/paintshop_ilp.rs index b5f455827..2de2f794d 100644 --- a/src/rules/paintshop_ilp.rs +++ b/src/rules/paintshop_ilp.rs @@ -28,8 +28,6 @@ impl ReductionResult for ReductionPaintShopToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_cars] .iter() .map(|&value| value == 1) @@ -117,7 +115,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec 3 cars - let source = PaintShop::new(vec!["A", "B", "A", "C", "B", "C"]); + let source = PaintShop::new(vec!["A", "B", "A", "C", "B", "C"]).unwrap(); let reduction: ReductionPaintShopToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target_config = { diff --git a/src/rules/paintshop_qubo.rs b/src/rules/paintshop_qubo.rs index b8a05456c..86a4805e2 100644 --- a/src/rules/paintshop_qubo.rs +++ b/src/rules/paintshop_qubo.rs @@ -32,8 +32,6 @@ impl ReductionResult for ReductionPaintShopToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -50,7 +48,7 @@ impl ReduceTo> for PaintShop { let is_first = self.is_first(); let seq_len = seq.len(); - let mut matrix = vec![vec![0i64; n]; n]; + let mut matrix = vec![std::collections::BTreeMap::new(); n]; let overflow = |operation| { crate::rules::ReductionError::integer_overflow::>(operation) }; @@ -74,32 +72,38 @@ impl ReduceTo> for PaintShop { if parity_a == parity_b { // Same parity: color change when x_a != x_b // Contribution: +1 to Q[a][a], +1 to Q[b][b], -2 to Q[lo][hi] - matrix[a][a] = matrix[a][a] + let coefficient = matrix[a].entry(a).or_insert(0i64); + *coefficient = coefficient .checked_add(1) .ok_or_else(|| overflow("adding a PaintShop diagonal coefficient"))?; - matrix[b][b] = matrix[b][b] + let coefficient = matrix[b].entry(b).or_insert(0i64); + *coefficient = coefficient .checked_add(1) .ok_or_else(|| overflow("adding a PaintShop diagonal coefficient"))?; - matrix[lo][hi] = matrix[lo][hi] + let coefficient = matrix[lo].entry(hi).or_insert(0i64); + *coefficient = coefficient .checked_sub(2) .ok_or_else(|| overflow("adding a PaintShop interaction coefficient"))?; } else { // Different parity: color change when x_a == x_b // Contribution: -1 to Q[a][a], -1 to Q[b][b], +2 to Q[lo][hi] - matrix[a][a] = matrix[a][a] + let coefficient = matrix[a].entry(a).or_insert(0i64); + *coefficient = coefficient .checked_sub(1) .ok_or_else(|| overflow("adding a PaintShop diagonal coefficient"))?; - matrix[b][b] = matrix[b][b] + let coefficient = matrix[b].entry(b).or_insert(0i64); + *coefficient = coefficient .checked_sub(1) .ok_or_else(|| overflow("adding a PaintShop diagonal coefficient"))?; - matrix[lo][hi] = matrix[lo][hi] + let coefficient = matrix[lo].entry(hi).or_insert(0i64); + *coefficient = coefficient .checked_add(2) .ok_or_else(|| overflow("adding a PaintShop interaction coefficient"))?; } } Ok(ReductionPaintShopToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { + target: QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::>(message) })?, }) @@ -114,7 +118,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/partiallyorderedknapsack_ilp.rs b/src/rules/partiallyorderedknapsack_ilp.rs index ecfef47b6..22ed8b600 100644 --- a/src/rules/partiallyorderedknapsack_ilp.rs +++ b/src/rules/partiallyorderedknapsack_ilp.rs @@ -25,8 +25,6 @@ impl ReductionResult for ReductionPOKToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -78,7 +76,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/partition_binpacking.rs b/src/rules/partition_binpacking.rs index 20301e8a5..639e603ec 100644 --- a/src/rules/partition_binpacking.rs +++ b/src/rules/partition_binpacking.rs @@ -34,8 +34,6 @@ impl ReductionResult for ReductionPartitionToBinPacking { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // BinPacking may use any bin indices (0..n-1). Remap the two distinct // bins used in a 2-bin packing to Partition's {0, 1} assignment. diff --git a/src/rules/partition_cosineproductintegration.rs b/src/rules/partition_cosineproductintegration.rs index 1698a5334..3a564b900 100644 --- a/src/rules/partition_cosineproductintegration.rs +++ b/src/rules/partition_cosineproductintegration.rs @@ -32,8 +32,6 @@ impl ReductionResult for ReductionPartitionToCPI { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -48,7 +46,8 @@ impl ReduceTo for Partition { fn reduce_to(&self) -> Result { let coefficients = self.sizes().to_vec(); Ok(ReductionPartitionToCPI { - target: CosineProductIntegration::new(coefficients), + target: CosineProductIntegration::new(coefficients) + .map_err(>::target_construction)?, }) } } diff --git a/src/rules/partition_integralflowwithmultipliers.rs b/src/rules/partition_integralflowwithmultipliers.rs index 8c1a96391..9b0afdf8b 100644 --- a/src/rules/partition_integralflowwithmultipliers.rs +++ b/src/rules/partition_integralflowwithmultipliers.rs @@ -15,7 +15,7 @@ use crate::topology::DirectedGraph; #[derive(Debug, Clone)] pub struct ReductionPartitionToIntegralFlowWithMultipliers { target: IntegralFlowWithMultipliers, - item_arc_count: Option, + item_arc_count: usize, } impl ReductionResult for ReductionPartitionToIntegralFlowWithMultipliers { @@ -31,14 +31,7 @@ impl ReductionResult for ReductionPartitionToIntegralFlowWithMultipliers { target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { Ok({ - let item_arc_count = self.item_arc_count.ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "the fixed infeasible target instance has no extractable witness", - ) - })?; - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - target_solution[..item_arc_count] + target_solution[..self.item_arc_count] .iter() .map(|&flow| flow > 0) .collect() @@ -64,10 +57,14 @@ impl ReduceTo for Partition { let source_n = self.num_elements(); if total_sum % 2 != 0 { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]) + .map_err(>::target_construction)?; return Ok(ReductionPartitionToIntegralFlowWithMultipliers { - target: IntegralFlowWithMultipliers::new(graph, 0, 2, vec![1, 2, 1], vec![1, 1], 1), - item_arc_count: None, + target: IntegralFlowWithMultipliers::new(graph, 0, 2, vec![1, 2, 1], vec![1, 1], 1) + .map_err( + >::target_construction, + )?, + item_arc_count: source_n, }); } @@ -96,7 +93,8 @@ impl ReduceTo for Partition { capacities.push(half_sum); multipliers[relay] = 1; - let graph = DirectedGraph::new(source_n + 3, arcs); + let graph = DirectedGraph::new(source_n + 3, arcs) + .map_err(>::target_construction)?; Ok(ReductionPartitionToIntegralFlowWithMultipliers { target: IntegralFlowWithMultipliers::new( graph, @@ -105,8 +103,9 @@ impl ReduceTo for Partition { multipliers, capacities, half_sum, - ), - item_arc_count: Some(source_n), + ) + .map_err(>::target_construction)?, + item_arc_count: source_n, }) } } diff --git a/src/rules/partition_knapsack.rs b/src/rules/partition_knapsack.rs index 6ea901cca..31ec43d50 100644 --- a/src/rules/partition_knapsack.rs +++ b/src/rules/partition_knapsack.rs @@ -22,8 +22,6 @@ impl ReductionResult for ReductionPartitionToKnapsack { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -43,7 +41,8 @@ impl ReduceTo for Partition { let capacity = self.total_sum() / 2; Ok(ReductionPartitionToKnapsack { - target: Knapsack::new(weights, values, capacity), + target: Knapsack::new(weights, values, capacity) + .map_err(>::target_construction)?, }) } } diff --git a/src/rules/partition_multiprocessorscheduling.rs b/src/rules/partition_multiprocessorscheduling.rs index 9e35ce1d2..4bc184c1a 100644 --- a/src/rules/partition_multiprocessorscheduling.rs +++ b/src/rules/partition_multiprocessorscheduling.rs @@ -36,8 +36,6 @@ impl ReductionResult for ReductionPartitionToMPS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution .iter() .map(|&processor| processor == 1) @@ -61,7 +59,8 @@ impl ReduceTo for Partition { let deadline = self.total_sum() / 2; Ok(ReductionPartitionToMPS { - target: MultiprocessorScheduling::new(lengths, 2, deadline), + target: MultiprocessorScheduling::new(lengths, 2, deadline) + .map_err(>::target_construction)?, }) } } diff --git a/src/rules/partition_openshopscheduling.rs b/src/rules/partition_openshopscheduling.rs index f3126f0c5..a660b5dc2 100644 --- a/src/rules/partition_openshopscheduling.rs +++ b/src/rules/partition_openshopscheduling.rs @@ -22,52 +22,26 @@ impl ReductionResult for ReductionPartitionToOpenShopScheduling { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target schedule does not certify a balanced partition", - )); - } - Ok({ let num_elements = self.target.num_jobs() - 1; let mut source_config = vec![false; num_elements]; let m = self.target.num_machines(); let start_times = target_solution .chunks_exact(m) - .map(|times| { - times - .iter() - .map(|&time| { - i64::try_from(time).map_err(|_| { - crate::rules::ExtractionError::invalid( - "target schedule time does not fit i64", - ) - }) - }) - .collect::, _>>() - }) - .collect::, _>>()?; + .map(|times| times.iter().map(|&time| time as i64).collect::>()) + .collect::>(); let special_job = num_elements; let half_sum = self.target.processing_times()[special_job][0]; // Find the middle machine where the special job starts at half_sum - let middle_machine = (0..m) - .find(|&machine| start_times[special_job][machine] == half_sum) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "target schedule has no machine at the partition boundary", - ) - })?; + let middle_machine: usize = (0..m) + .filter(|&machine| start_times[special_job][machine] == half_sum) + .sum(); let pivot = start_times[special_job][middle_machine]; for (job, slot) in source_config.iter_mut().enumerate() { let completion = start_times[job][middle_machine] - .checked_add(self.target.processing_times()[job][middle_machine]) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid("target schedule time overflows i64") - })?; + + self.target.processing_times()[job][middle_machine]; if completion <= pivot { *slot = true; } diff --git a/src/rules/partition_productionplanning.rs b/src/rules/partition_productionplanning.rs index 3dc8856fb..a56b96a01 100644 --- a/src/rules/partition_productionplanning.rs +++ b/src/rules/partition_productionplanning.rs @@ -21,8 +21,6 @@ impl ReductionResult for ReductionPartitionToProductionPlanning { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.target.num_periods() - 1] .iter() .map(|&production| production > 0) @@ -66,7 +64,8 @@ impl ReduceTo for Partition { production_costs, inventory_costs, half_floor, - ), + ) + .map_err(>::target_construction)?, }) } } diff --git a/src/rules/partition_sequencingtominimizetardytaskweight.rs b/src/rules/partition_sequencingtominimizetardytaskweight.rs index 72827a5d0..b13ef760d 100644 --- a/src/rules/partition_sequencingtominimizetardytaskweight.rs +++ b/src/rules/partition_sequencingtominimizetardytaskweight.rs @@ -22,26 +22,12 @@ impl ReductionResult for ReductionPartitionToSequencingToMinimizeTardyTaskWeight &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target schedule does not certify a balanced partition", - )); - } - Ok({ let mut source_config = vec![true; self.target.num_tasks()]; let mut completion_time = 0i64; for &task in target_solution { - completion_time = completion_time - .checked_add(self.target.lengths()[task]) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "target schedule completion time overflows i64", - ) - })?; + completion_time += self.target.lengths()[task]; if completion_time <= self.target.deadlines()[task] { source_config[task] = false; } diff --git a/src/rules/partition_subsetsum.rs b/src/rules/partition_subsetsum.rs index 57f6c0d14..358f39ac9 100644 --- a/src/rules/partition_subsetsum.rs +++ b/src/rules/partition_subsetsum.rs @@ -13,9 +13,6 @@ use num_bigint::{BigUint, ToBigUint}; #[derive(Debug, Clone)] pub struct ReductionPartitionToSubsetSum { target: SubsetSum, - /// Number of elements in the original Partition instance. - /// When the total sum is odd, the target has 0 elements but the source has n. - source_n: usize, } impl ReductionResult for ReductionPartitionToSubsetSum { @@ -30,15 +27,6 @@ impl ReductionResult for ReductionPartitionToSubsetSum { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - if target_solution.len() != self.source_n { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} subset-selection values, got {}", - self.source_n, - target_solution.len() - ))); - } Ok(target_solution.to_vec()) } } @@ -52,14 +40,12 @@ impl ReduceTo for Partition { fn reduce_to(&self) -> Result { let total = self.total_sum(); - let source_n = self.num_elements(); Ok(if total % 2 != 0 { // Odd total sum: no balanced partition exists. // Return a trivially infeasible SubsetSum: no elements, target = 1. ReductionPartitionToSubsetSum { target: SubsetSum::new_unchecked(vec![], BigUint::from(1u32)), - source_n, } } else { let sizes: Vec = self @@ -75,7 +61,6 @@ impl ReduceTo for Partition { .expect("validated nonnegative Partition total"); ReductionPartitionToSubsetSum { target: SubsetSum::new_unchecked(sizes, target_val), - source_n, } }) } diff --git a/src/rules/partition_sumofsquarespartition.rs b/src/rules/partition_sumofsquarespartition.rs index 8eb491c9b..6aa16d8c7 100644 --- a/src/rules/partition_sumofsquarespartition.rs +++ b/src/rules/partition_sumofsquarespartition.rs @@ -49,15 +49,6 @@ impl ReductionResult for ReductionPartitionToSumOfSquaresPartition { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if target_solution.len() != self.target.num_elements() { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} target group assignments, got {}", - self.target.num_elements(), - target_solution.len() - ))); - } - Ok(target_solution[..self.source_n] .iter() .map(|&group| group == 1) diff --git a/src/rules/partitionintocliques_ilp.rs b/src/rules/partitionintocliques_ilp.rs index 843c6a905..272f8c3d0 100644 --- a/src/rules/partitionintocliques_ilp.rs +++ b/src/rules/partitionintocliques_ilp.rs @@ -25,19 +25,12 @@ impl ReductionResult for ReductionPartitionIntoCliquesToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - (0..self.num_vertices) - .map(|vertex| { - (0..self.num_cliques) - .find(|&clique| target_solution[vertex * self.num_cliques + clique] == 1) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "target solution does not assign vertex {vertex} to a clique" - )) - }) - }) - .collect() + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.num_vertices, + self.num_cliques, + 0, + )) } } @@ -100,7 +93,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/partitionintocliques_minimumcoveringbycliques.rs b/src/rules/partitionintocliques_minimumcoveringbycliques.rs index b3078e508..68fafd4d1 100644 --- a/src/rules/partitionintocliques_minimumcoveringbycliques.rs +++ b/src/rules/partitionintocliques_minimumcoveringbycliques.rs @@ -142,7 +142,6 @@ fn target_clique_bound( pub struct ReductionPartitionIntoCliquesToMinimumCoveringByCliques { target: MinimumCoveringByCliques, num_source_vertices: usize, - source_num_cliques: usize, target_bound: i64, } @@ -158,58 +157,32 @@ impl ReductionResult for ReductionPartitionIntoCliquesToMinimumCoveringByCliques &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !Min::meets_bound(&value, &self.target_bound) { - return Err(crate::rules::ExtractionError::invalid( - "target cover does not certify the source clique bound", - )); - } - - Ok({ - let n = self.num_source_vertices; - let target_edges = self.target.graph().edges(); - let mut matching_labels = vec![None; n]; - for ((u, v), &label) in target_edges.iter().zip(target_solution.iter()) { - let matching_index = if *u < n && *v == n + *u { - Some(*u) - } else if *v < n && *u == n + *v { - Some(*v) + let n = self.num_source_vertices; + let mut matching_labels: Vec<_> = self + .target + .graph() + .edges() + .into_iter() + .zip(target_solution) + .filter_map(|((u, v), &label)| { + if u < n && v == n + u { + Some((u, label)) + } else if v < n && u == n + v { + Some((v, label)) } else { None - }; - - if let Some(i) = matching_index { - matching_labels[i] = Some(label); } - } - - let mut label_map = BTreeMap::new(); - let extracted = matching_labels - .into_iter() - .map(|label| { - let label = label.ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "target cover does not label every matching gadget edge", - ) - })?; - let next = label_map.len(); - Ok(*label_map.entry(label).or_insert(next)) - }) - .collect::>>()?; - - if label_map.len() > self.source_num_cliques { - return Err(crate::rules::ExtractionError::invalid(format!( - "target cover uses {} cliques, exceeding source bound {}", - label_map.len(), - self.source_num_cliques - ))); - } - - // Equal matching-edge labels imply pairwise source adjacency. - // The target certificate leaves at most K labels for these edges. - extracted - }) + }) + .collect(); + matching_labels.sort_unstable_by_key(|&(vertex, _)| vertex); + let mut label_map = BTreeMap::new(); + Ok(matching_labels + .into_iter() + .map(|(_, label)| { + let next = label_map.len(); + *label_map.entry(label).or_insert(next) + }) + .collect()) } } @@ -281,13 +254,14 @@ impl ReduceTo> for PartitionIntoCliques>>::target_construction, + )?; let target = MinimumCoveringByCliques::new(target_graph); Ok(ReductionPartitionIntoCliquesToMinimumCoveringByCliques { target, num_source_vertices: n, - source_num_cliques: self.num_cliques(), target_bound, }) } @@ -324,7 +298,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source) .expect("reduction should succeed"); let layout = OrlinLayout::new(source.graph()); diff --git a/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs b/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs index 193a5d681..d6300b6c1 100644 --- a/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs +++ b/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs @@ -37,8 +37,6 @@ impl ReductionResult for ReductionPPL2ToBCSF { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -63,11 +61,11 @@ impl ReduceTo> let max_components = if q == 0 { 1 } else { q }; let target = BoundedComponentSpanningForest::new( - SimpleGraph::new(n, self.graph().edges()), + SimpleGraph::new(n, self.graph().edges()).map_err(>>::target_construction)?, vec![1i64; n], // unit weights max_components, // K = max(|V|/3, 1) 3, // B = 3 - ); + ).map_err(>>::target_construction)?; Ok(ReductionPPL2ToBCSF { target }) } @@ -81,10 +79,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, diff --git a/src/rules/partitionintopathsoflength2_ilp.rs b/src/rules/partitionintopathsoflength2_ilp.rs index f652ba758..a0a335755 100644 --- a/src/rules/partitionintopathsoflength2_ilp.rs +++ b/src/rules/partitionintopathsoflength2_ilp.rs @@ -48,14 +48,12 @@ impl ReductionResult for ReductionPIPL2ToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_vertices, self.num_groups, 0, - ) + )) } } @@ -129,10 +127,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/partitionintotriangles_ilp.rs b/src/rules/partitionintotriangles_ilp.rs index b80d3fe8c..7c2ac5d7e 100644 --- a/src/rules/partitionintotriangles_ilp.rs +++ b/src/rules/partitionintotriangles_ilp.rs @@ -41,14 +41,12 @@ impl ReductionResult for ReductionPITToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_vertices, self.num_groups, 0, - ) + )) } } @@ -116,10 +114,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/pathconstrainednetworkflow_ilp.rs b/src/rules/pathconstrainednetworkflow_ilp.rs index 799cebfc8..dcbd68b32 100644 --- a/src/rules/pathconstrainednetworkflow_ilp.rs +++ b/src/rules/pathconstrainednetworkflow_ilp.rs @@ -26,8 +26,6 @@ impl ReductionResult for ReductionPCNFToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(target_solution) } } @@ -84,7 +82,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec1->2 and 0->2 // Two paths: [0,1] (0->1->2) and [2] (0->2) let source = PathConstrainedNetworkFlow::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1, 1, 1], 0, 2, diff --git a/src/rules/precedenceconstrainedscheduling_ilp.rs b/src/rules/precedenceconstrainedscheduling_ilp.rs index c79cbc5fc..25facaba6 100644 --- a/src/rules/precedenceconstrainedscheduling_ilp.rs +++ b/src/rules/precedenceconstrainedscheduling_ilp.rs @@ -42,14 +42,12 @@ impl ReductionResult for ReductionPCSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_tasks, self.deadline, 0, - ) + )) } } @@ -121,7 +119,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/preemptivescheduling_ilp.rs b/src/rules/preemptivescheduling_ilp.rs index f6d65c971..0d390f9f3 100644 --- a/src/rules/preemptivescheduling_ilp.rs +++ b/src/rules/preemptivescheduling_ilp.rs @@ -55,8 +55,6 @@ impl ReductionResult for ReductionPSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok((0..self.num_tasks) .map(|task| { (0..self.d_max) diff --git a/src/rules/prizecollectingsteinerforest_steinertree.rs b/src/rules/prizecollectingsteinerforest_steinertree.rs index 26ab8ddb6..468e98168 100644 --- a/src/rules/prizecollectingsteinerforest_steinertree.rs +++ b/src/rules/prizecollectingsteinerforest_steinertree.rs @@ -16,11 +16,13 @@ //! - every `v in V` is attached to `r` by an edge of cost `omega` (so each //! tree component of `F` is paid by exactly one root-attachment edge in //! `T*`), -//! - for every `v in V_p` we add `(v, t_v)` of cost `0` and `(r, t_v)` of -//! cost `beta * p(v)`, +//! - with `M = omega + 1`, for every `v in V_p` add `(v, t_v)` of cost `M` +//! and `(r, t_v)` of cost `M + beta * p(v)`, //! - the terminal set is `{r} cup {t_v : v in V_p}`. //! -//! The Steiner-tree optimum then equals the PCSF optimum. +//! The Steiner-tree optimum equals the PCSF optimum plus `M * |V_p|`. +//! In an optimum each gadget terminal is a leaf: replacing both gadget edges +//! by the include edge and a root attachment strictly reduces cost. //! //! References: //! - Bienstock, Goemans, Simchi-Levi, Williamson, "A note on the prize @@ -38,7 +40,7 @@ use crate::topology::{Graph, SimpleGraph}; /// Result of reducing PCSF to SteinerTree. /// -/// Stores the original PCSF source parameterss plus the mapping from the target +/// Stores the original PCSF source parameters plus the mapping from the target /// graph's edge list back to the source variables (the original edge index /// for each "original" edge, and the source vertex index for each gadget /// include-edge). Other target edges (root-attachment and gadget omit-edges) @@ -73,13 +75,12 @@ impl ReductionResult for ReductionPCSFToSteinerTree { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_source_vertices; let m = self.num_source_edges; let mut selected_vertices = vec![false; n]; let mut selected_edges = vec![false; m]; + let edges = self.target.graph().edges(); // Mark vertices included via their gadget include-edge `(v, t_v)`, // and edges via the matching original edge. @@ -91,20 +92,9 @@ impl ReductionResult for ReductionPCSFToSteinerTree { selected_vertices[v] = true; } else if let Some(src_edge) = self.target_to_source_edge[target_idx] { selected_edges[src_edge] = true; - } - } - - // Any original edge selected in `T*` forces both endpoints into - // `V_F`. The PCSF model rejects configurations where a selected - // edge has an unselected endpoint, so we mark endpoints explicitly - // (this also covers prize-zero endpoints, which have no gadget). - let edges = self.target.graph().edges(); - for (target_idx, &(_, _)) in edges.iter().enumerate() { - if !target_solution[target_idx] { - continue; - } - if let Some(src_edge) = self.target_to_source_edge[target_idx] { - let (u, v) = self.source_edge_pair(src_edge); + // Include both endpoints, including prize-zero vertices + // that have no inclusion gadget. + let (u, v) = edges[target_idx]; selected_vertices[u] = true; selected_vertices[v] = true; } @@ -115,14 +105,6 @@ impl ReductionResult for ReductionPCSFToSteinerTree { } } -impl ReductionPCSFToSteinerTree { - /// Look up the endpoint pair of the `idx`-th source edge in the target - /// graph's edge list (source edges are placed first by construction). - fn source_edge_pair(&self, src_edge_idx: usize) -> (usize, usize) { - self.target.graph().edges()[src_edge_idx] - } -} - #[reduction( transform = exact { num_vertices = "num_vertices + num_vertices_with_prize + 1", @@ -174,18 +156,34 @@ impl ReduceTo> for PrizeCollectingSteinerForest, + >("forming the Steiner gadget inclusion cost") + })?; + let omit_cost = beta + .checked_mul(source_prizes[v]) + .and_then(|penalty| penalty.checked_add(include_cost)) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::< + Self, + SteinerTree, + >("forming the Steiner gadget omission cost") + })?; + // The include edge records a selected prized vertex. target_edges.push((v, t_v)); - target_edge_weights.push(0); + target_edge_weights.push(include_cost); target_to_source_edge.push(None); target_to_include_vertex.push(Some(v)); - // omit-edge: pays beta * p(v) when v is excluded from V_F. + // The omit edge pays the extra beta * p(v). target_edges.push((root, t_v)); - target_edge_weights.push(beta * source_prizes[v]); + target_edge_weights.push(omit_cost); target_to_source_edge.push(None); target_to_include_vertex.push(None); } @@ -197,7 +195,8 @@ impl ReduceTo> for PrizeCollectingSteinerForest>>::target_construction)?; let target = SteinerTree::::new(target_graph, target_edge_weights, terminals); @@ -215,43 +214,26 @@ impl ReduceTo> for PrizeCollectingSteinerForest Vec { use crate::example_db::specs::RuleExampleSpec; use crate::export::SolutionPair; - use crate::solvers::BruteForce; vec![RuleExampleSpec { id: "prize_collecting_steiner_forest_to_steiner_tree", build: || { - // Issue #1027 canonical instance with the omit-edge actually - // selected at the optimum: path 0 - 1 - 2 with c(0,1)=10, - // c(1,2)=10, prizes p = (5, 1, 5), beta = 1, omega = 1. The - // optimum drops vertex 1 (paying p(1) = 1) rather than paying a - // size-10 edge to reach it. let source = PrizeCollectingSteinerForest::::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![5, 1, 5], vec![10, 10], 1, 1, ) .unwrap(); - let reduction = as ReduceTo< - SteinerTree, - >>::reduce_to(&source) - .expect("reduction should succeed"); - let target = reduction.target_problem(); - let target_config = BruteForce::new() - .solve(target) - .expect("canonical target evaluation must succeed") - .expect("canonical PCSF -> SteinerTree example must have an optimal target tree"); - let source_config = reduction.extract_solution(&target_config).unwrap(); - crate::example_db::specs::assemble_rule_example( - &source, - target, - vec![SolutionPair { - source_config: serde_json::to_value(source_config) - .expect("solution serialization must succeed"), - target_config: serde_json::to_value(target_config) - .expect("solution serialization must succeed"), - }], + crate::example_db::specs::rule_example_with_witness::<_, SteinerTree>( + source, + SolutionPair { + source_config: serde_json::json!([[true, false, true], [false, false]]), + target_config: serde_json::json!([ + false, false, true, false, true, true, false, false, true, true, false + ]), + }, ) }, }] diff --git a/src/rules/quadraticassignment_ilp.rs b/src/rules/quadraticassignment_ilp.rs index aacef5d4e..8ff96040e 100644 --- a/src/rules/quadraticassignment_ilp.rs +++ b/src/rules/quadraticassignment_ilp.rs @@ -38,14 +38,12 @@ impl ReductionResult for ReductionQAPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_facilities, self.num_locations, 0, - ) + )) } } @@ -135,7 +133,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/qubo_casts.rs b/src/rules/qubo_casts.rs index 841c432cc..08639d9be 100644 --- a/src/rules/qubo_casts.rs +++ b/src/rules/qubo_casts.rs @@ -10,20 +10,16 @@ impl_variant_reduction!( => , fields: [num_vars], |src| { - let matrix = src - .matrix() - .iter() - .map(|row| { - row.iter() - .copied() - .map(i64_to_exact_f64) - .collect::, _>>() - }) - .collect::, _>>() - .map_err(|error| { - ReductionError::inexact_float_conversion::, QUBO>(error) - })?; - QUBO::from_matrix(matrix) + let coefficients = src.matrix().data().iter().copied() + .map(i64_to_exact_f64).collect::, _>>() + .map_err(ReductionError::inexact_float_conversion::, QUBO>)?; + let matrix = sprs::CsMat::new( + src.matrix().shape(), + src.matrix().indptr().raw_storage().to_vec(), + src.matrix().indices().to_vec(), + coefficients, + ); + QUBO::from_sparse(matrix) .map_err(ReductionError::construction::, QUBO>)? } ); diff --git a/src/rules/qubo_ilp.rs b/src/rules/qubo_ilp.rs index 2dd22909c..9d2f6e1e0 100644 --- a/src/rules/qubo_ilp.rs +++ b/src/rules/qubo_ilp.rs @@ -41,8 +41,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_original] .iter() .map(|&value| value == 1) @@ -59,9 +57,9 @@ where // Collect non-zero off-diagonal entries (i < j) let mut off_diag: Vec<(usize, usize, C)> = Vec::new(); - for (i, row) in matrix.iter().enumerate() { - for (j, &q_ij) in row.iter().enumerate().skip(i + 1) { - if q_ij != C::zero() { + for (i, row) in matrix.outer_iterator().enumerate() { + for (j, &q_ij) in row.iter() { + if j > i && q_ij != C::zero() { off_diag.push((i, j, q_ij)); } } @@ -72,8 +70,8 @@ where // Objective: minimize Σ Q_ii · x_i + Σ Q_ij · y_k let mut objective: Vec<(usize, C)> = Vec::new(); - for (i, row) in matrix.iter().enumerate() { - let q_ii = row[i]; + for (i, row) in matrix.outer_iterator().enumerate() { + let q_ii = row.get(i).copied().unwrap_or_else(C::zero); if q_ii != C::zero() { objective.push((i, q_ii)); } diff --git a/src/rules/rectilinearpicturecompression_ilp.rs b/src/rules/rectilinearpicturecompression_ilp.rs index cf75fb3a1..94d7a2c20 100644 --- a/src/rules/rectilinearpicturecompression_ilp.rs +++ b/src/rules/rectilinearpicturecompression_ilp.rs @@ -25,8 +25,6 @@ impl ReductionResult for ReductionRPCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -79,7 +77,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/registersufficiency_ilp.rs b/src/rules/registersufficiency_ilp.rs index 021b02edc..2e178bf8d 100644 --- a/src/rules/registersufficiency_ilp.rs +++ b/src/rules/registersufficiency_ilp.rs @@ -30,8 +30,6 @@ impl ReductionResult for ReductionRegisterSufficiencyToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(&target_solution[..self.num_vertices]) } } @@ -204,7 +202,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/registry.rs b/src/rules/registry.rs index 80a10beb6..7f0b7bc1a 100644 --- a/src/rules/registry.rs +++ b/src/rules/registry.rs @@ -132,9 +132,22 @@ impl From for ParameterContractError { } } +/// Interpret an accepted target optimum using this execution's mathematical relation. +pub type InterpretOptimum = dyn Fn(&dyn Any) -> crate::rules::ExtractionResult; + +/// One executed witness reduction, with optional value mapping over the same state. +#[derive(Clone)] +pub struct ExecutedStep { + /// Target access and witness recovery for this execution. + pub witness: std::rc::Rc, + /// Value recovery sharing the witness result allocation, when supported. + pub aggregate: Option>, + /// Solver completion only: whether the mapped optimum supplies a source witness. + pub interpret_optimum: Option>, +} + /// Witness/config reduction executor stored in the inventory. -pub type ReduceFn = - fn(&dyn Any) -> Result, crate::rules::ReductionError>; +pub type ReduceFn = fn(&dyn Any) -> Result; /// Aggregate/value reduction executor stored in the inventory. pub type AggregateReduceFn = @@ -182,7 +195,7 @@ pub struct ReductionEntry { pub module_path: &'static str, /// Type-erased reduction executor. /// Takes a `&dyn Any` (must be `&SourceType`), calls `ReduceTo::reduce_to()`, - /// and returns either a boxed `DynReductionResult` or the edge's `ReductionError`. + /// and returns one `ExecutedStep` sharing the result, or the edge's `ReductionError`. pub reduce_fn: Option, /// Type-erased aggregate reduction executor. /// Takes a `&dyn Any` (must be `&SourceType`), calls diff --git a/src/rules/resourceconstrainedscheduling_ilp.rs b/src/rules/resourceconstrainedscheduling_ilp.rs index bcc1726d9..9bbbcea69 100644 --- a/src/rules/resourceconstrainedscheduling_ilp.rs +++ b/src/rules/resourceconstrainedscheduling_ilp.rs @@ -33,14 +33,12 @@ impl ReductionResult for ReductionRCSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_tasks, self.deadline, 0, - ) + )) } } diff --git a/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs b/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs index 8c40f906a..4fb79e79a 100644 --- a/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs +++ b/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs @@ -40,8 +40,6 @@ impl ReductionResult for ReductionRootedTreeArrangementToRootedTreeStorageAssign &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_vertices; // target_solution is the parent array of the rooted tree on X = V @@ -126,8 +124,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( diff --git a/src/rules/rootedtreestorageassignment_ilp.rs b/src/rules/rootedtreestorageassignment_ilp.rs index 70b4d9c56..46d67fcb8 100644 --- a/src/rules/rootedtreestorageassignment_ilp.rs +++ b/src/rules/rootedtreestorageassignment_ilp.rs @@ -76,9 +76,7 @@ impl ReductionResult for ReductionRTSAToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows(target_solution, self.n, self.n, 0) + Ok(one_hot_decode_rows(target_solution, self.n, self.n, 0)) } } diff --git a/src/rules/ruralpostman_ilp.rs b/src/rules/ruralpostman_ilp.rs index ad8e63a1d..87f045cc7 100644 --- a/src/rules/ruralpostman_ilp.rs +++ b/src/rules/ruralpostman_ilp.rs @@ -30,8 +30,6 @@ impl ReductionResult for ReductionRPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if self.target.num_vars() == 0 { Ok(vec![0; self.num_edges]) } else { @@ -221,10 +219,11 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/sat_circuitsat.rs b/src/rules/sat_circuitsat.rs index c6fbfab3e..e18f6ca6e 100644 --- a/src/rules/sat_circuitsat.rs +++ b/src/rules/sat_circuitsat.rs @@ -7,7 +7,6 @@ use crate::models::formula::Satisfiability; use crate::models::formula::{Assignment, BooleanExpr, Circuit, CircuitSAT}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -use crate::solvers::BruteForceProblem as _; use std::collections::HashSet; /// Result of reducing SAT to CircuitSAT. @@ -30,8 +29,6 @@ impl ReductionResult for ReductionSATToCircuit { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ self.source_var_indices .iter() @@ -55,7 +52,7 @@ impl ReduceTo for Satisfiability { type Result = ReductionSATToCircuit; fn reduce_to(&self) -> Result { - let num_vars = self.num_variables(); + let num_vars = self.num_vars(); let clauses = self.clauses(); let mut assignments = Vec::new(); diff --git a/src/rules/sat_coloring.rs b/src/rules/sat_coloring.rs index 09fadc3b2..c0bfe13bb 100644 --- a/src/rules/sat_coloring.rs +++ b/src/rules/sat_coloring.rs @@ -198,8 +198,13 @@ impl SATColoringConstructor { } /// Build the final KColoring problem. - fn build_coloring(&self) -> KColoring { - KColoring::::new(SimpleGraph::new(self.num_vertices, self.edges.clone())) + fn build_coloring( + &self, + ) -> Result, crate::registry::ConstructionError> { + Ok(KColoring::::new(SimpleGraph::new( + self.num_vertices, + self.edges.clone(), + )?)) } } @@ -244,33 +249,15 @@ impl ReductionResult for ReductionSATToColoring { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // First determine which color is TRUE, FALSE, and AUX // Vertices 0, 1, 2 are TRUE, FALSE, AUX respectively let true_color = target_solution[0]; - let false_color = target_solution[1]; - let aux_color = target_solution[2]; - - if true_color == false_color || true_color == aux_color || false_color == aux_color { - return Err(crate::rules::ExtractionError::invalid( - "target coloring does not distinguish true, false, and auxiliary colors", - )); - } - let mut assignment = vec![false; self.num_source_variables]; for (i, &pos_vertex) in self.pos_vertices.iter().enumerate() { let vertex_color = target_solution[pos_vertex]; - // Sanity check: variable vertices should not have AUX color - if vertex_color == aux_color { - return Err(crate::rules::ExtractionError::invalid(format!( - "variable {i} has the auxiliary color" - ))); - } - // If positive literal has TRUE color, variable is true (1) // Otherwise, variable is false (0) assignment[i] = vertex_color == true_color; @@ -316,7 +303,9 @@ impl ReduceTo> for Satisfiability { constructor.add_clause(&clause.literals); } - let target = constructor.build_coloring(); + let target = constructor + .build_coloring() + .map_err(>>::target_construction)?; Ok(ReductionSATToColoring { target, diff --git a/src/rules/sat_ksat.rs b/src/rules/sat_ksat.rs index 897c8bb5b..9d27c416f 100644 --- a/src/rules/sat_ksat.rs +++ b/src/rules/sat_ksat.rs @@ -36,8 +36,6 @@ impl ReductionResult for ReductionSATToKSAT { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Only return the original variables, discarding ancillas target_solution[..self.source_num_vars].to_vec() @@ -186,8 +184,6 @@ impl ReductionResult for ReductionKSATToSAT { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Direct mapping - no transformation needed target_solution.to_vec() diff --git a/src/rules/sat_maximumindependentset.rs b/src/rules/sat_maximumindependentset.rs index d8271f74e..37bd520cd 100644 --- a/src/rules/sat_maximumindependentset.rs +++ b/src/rules/sat_maximumindependentset.rs @@ -82,15 +82,6 @@ impl ReductionResult for ReductionSATToIS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let certificate = crate::rules::AggregateReductionResult::extract_value(self, value); - if !certificate.0 { - return Err(crate::rules::ExtractionError::invalid( - "target independent set does not certify satisfiability", - )); - } - let mut assignment = vec![false; self.num_source_variables]; for (literal, &selected) in self.literals.iter().zip(target_solution) { if selected { @@ -175,9 +166,14 @@ impl ReduceTo> for Satisfiability { } let target = MaximumIndependentSet::new( - SimpleGraph::new(vertex_count, edges), + SimpleGraph::new(vertex_count, edges).map_err( + >>::target_construction, + )?, vec![One; vertex_count], - ); + ) + .map_err( + >>::target_construction, + )?; Ok(ReductionSATToIS { target, diff --git a/src/rules/sat_minimumdominatingset.rs b/src/rules/sat_minimumdominatingset.rs index bed0c45e8..3d2ab5cc6 100644 --- a/src/rules/sat_minimumdominatingset.rs +++ b/src/rules/sat_minimumdominatingset.rs @@ -62,15 +62,6 @@ impl ReductionResult for ReductionSATToDS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let certificate = crate::rules::AggregateReductionResult::extract_value(self, value); - if !certificate.0 { - return Err(crate::rules::ExtractionError::invalid( - "target dominating set does not certify satisfiability", - )); - } - let mut assignment = vec![false; self.num_literals]; for (&variable, &gadget) in &self.variables { assignment[variable] = target_solution[3 * gadget]; @@ -191,9 +182,12 @@ impl ReduceTo> for Satisfiability { } let target = MinimumDominatingSet::new( - SimpleGraph::new(num_vertices, edges), + SimpleGraph::new(num_vertices, edges).map_err( + >>::target_construction, + )?, vec![1i64; num_vertices], - ); + ) + .map_err(>>::target_construction)?; Ok(ReductionSATToDS { target, diff --git a/src/rules/satisfiability_integralflowhomologousarcs.rs b/src/rules/satisfiability_integralflowhomologousarcs.rs index d6a1fa8dc..1e406819d 100644 --- a/src/rules/satisfiability_integralflowhomologousarcs.rs +++ b/src/rules/satisfiability_integralflowhomologousarcs.rs @@ -106,8 +106,6 @@ impl ReductionResult for ReductionSATToIntegralFlowHomologousArcs { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ self.variable_paths .iter() @@ -253,13 +251,15 @@ impl ReduceTo for Satisfiability { Ok(ReductionSATToIntegralFlowHomologousArcs { target: IntegralFlowHomologousArcs::new( - DirectedGraph::new(indexer.total_vertices(), arcs), + DirectedGraph::new(indexer.total_vertices(), arcs) + .map_err(>::target_construction)?, capacities, indexer.source(), indexer.sink(), requirement, homologous_pairs, - ), + ) + .map_err(>::target_construction)?, variable_paths, }) } diff --git a/src/rules/satisfiability_maximum2satisfiability.rs b/src/rules/satisfiability_maximum2satisfiability.rs index 040837da5..e477085c1 100644 --- a/src/rules/satisfiability_maximum2satisfiability.rs +++ b/src/rules/satisfiability_maximum2satisfiability.rs @@ -26,15 +26,6 @@ impl ReductionResult for ReductionSatisfiabilityToMaximum2Satisfiability { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let certificate = crate::rules::AggregateReductionResult::extract_value(self, value); - if !certificate.0 { - return Err(crate::rules::ExtractionError::invalid( - "target assignment does not certify satisfiability", - )); - } - Ok(target_solution[..self.source_num_vars].to_vec()) } } diff --git a/src/rules/satisfiability_naesatisfiability.rs b/src/rules/satisfiability_naesatisfiability.rs index 0be93eb62..fd3543ed9 100644 --- a/src/rules/satisfiability_naesatisfiability.rs +++ b/src/rules/satisfiability_naesatisfiability.rs @@ -33,16 +33,7 @@ impl ReductionResult for ReductionSATToNAESAT { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let n = self.source_num_vars; - if target_solution.len() != n + 1 { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} target truth values, got {}", - n + 1, - target_solution.len() - ))); - } let sentinel = target_solution[n]; Ok(target_solution[..n] .iter() diff --git a/src/rules/satisfiability_nontautology.rs b/src/rules/satisfiability_nontautology.rs index 3ba7ee1c8..d932cc773 100644 --- a/src/rules/satisfiability_nontautology.rs +++ b/src/rules/satisfiability_nontautology.rs @@ -25,8 +25,6 @@ impl ReductionResult for ReductionSATToNonTautology { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs index caefabe83..17e80dfa3 100644 --- a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs @@ -56,9 +56,12 @@ impl ReductionResult for ReductionSMWCTToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows(target_solution, self.num_tasks, self.num_processors, 0) + Ok(one_hot_decode_rows( + target_solution, + self.num_tasks, + self.num_processors, + 0, + )) } } diff --git a/src/rules/schedulingwithindividualdeadlines_ilp.rs b/src/rules/schedulingwithindividualdeadlines_ilp.rs index a36ee872d..d7e21cb60 100644 --- a/src/rules/schedulingwithindividualdeadlines_ilp.rs +++ b/src/rules/schedulingwithindividualdeadlines_ilp.rs @@ -43,9 +43,12 @@ impl ReductionResult for ReductionSWIDToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows(target_solution, self.num_tasks, self.max_deadline, 0) + Ok(one_hot_decode_rows( + target_solution, + self.num_tasks, + self.max_deadline, + 0, + )) } } @@ -146,7 +149,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs index 1c87717e3..f350cd09e 100644 --- a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs +++ b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs @@ -35,12 +35,10 @@ impl ReductionResult for ReductionSTMMCCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_tasks; - one_hot_decode(target_solution, n, n, 0)? + one_hot_decode(target_solution, n, n, 0) }) } } diff --git a/src/rules/sequencingtominimizetardytaskweight_ilp.rs b/src/rules/sequencingtominimizetardytaskweight_ilp.rs index 5f41493d2..69413f461 100644 --- a/src/rules/sequencingtominimizetardytaskweight_ilp.rs +++ b/src/rules/sequencingtominimizetardytaskweight_ilp.rs @@ -29,20 +29,12 @@ impl ReductionResult for ReductionSTMTTWToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.is_valid() { - return Err(crate::rules::ExtractionError::invalid( - "target ILP assignment is infeasible", - )); - } - Ok({ let n = self.num_tasks; // Decode the n*n block of x_{j,p} variables into a schedule permutation. // The source uses direct permutation encoding (config = schedule directly), // so return the schedule as-is (it is already a permutation of 0..n). - one_hot_decode(target_solution, n, n, 0)? + one_hot_decode(target_solution, n, n, 0) }) } } diff --git a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index 1171c160a..dd9978ac5 100644 --- a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -41,8 +41,6 @@ impl ReductionResult for ReductionSTMWCTToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let mut schedule: Vec = (0..self.num_tasks).collect(); schedule.sort_by_key(|&task| (target_solution[task], task)); diff --git a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs index f046bd232..d7301a58f 100644 --- a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -37,8 +37,6 @@ impl ReductionResult for ReductionSTMWTToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_tasks; let c_offset = self.num_order_vars; @@ -166,7 +164,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs index 21cd366b0..6a7a15b7c 100644 --- a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs +++ b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs @@ -40,12 +40,10 @@ impl ReductionResult for ReductionSWDSTToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_tasks; // x_{j,p} occupies the first n*n variables: decode the permutation. - one_hot_decode(target_solution, n, n, 0)? + one_hot_decode(target_solution, n, n, 0) }) } } diff --git a/src/rules/sequencingwithinintervals_ilp.rs b/src/rules/sequencingwithinintervals_ilp.rs index daccfd111..8235c9af4 100644 --- a/src/rules/sequencingwithinintervals_ilp.rs +++ b/src/rules/sequencingwithinintervals_ilp.rs @@ -47,24 +47,15 @@ impl ReductionResult for ReductionSWIToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - self.task_layout + Ok(self + .task_layout .iter() - .enumerate() - .map(|(task, &(base, count))| { - let mut selected = (0..count).filter(|&offset| target_solution[base + offset] == 1); - match (selected.next(), selected.next()) { - (Some(offset), None) => Ok(offset), - (None, _) => Err(crate::rules::ExtractionError::invalid(format!( - "task {task} has no selected start time" - ))), - (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( - "task {task} has multiple selected start times" - ))), - } + .map(|&(base, count)| { + (0..count) + .filter(|&offset| target_solution[base + offset] == 1) + .sum() }) - .collect() + .collect()) } } diff --git a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs index d14ad874c..3aedde7dc 100644 --- a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs +++ b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs @@ -33,14 +33,12 @@ impl ReductionResult for ReductionSWRTDToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_tasks; let horizon = self.time_horizon; // For each task, find the start time let starts = - crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, n, horizon, 0)?; + crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, n, horizon, 0); let mut start_times: Vec<_> = starts.into_iter().enumerate().collect(); // Sort by start time (break ties by task index) start_times.sort_by_key(|&(j, t)| (t, j)); @@ -137,7 +135,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/setsplitting_betweenness.rs b/src/rules/setsplitting_betweenness.rs index a62f799d4..5f5c9ff5e 100644 --- a/src/rules/setsplitting_betweenness.rs +++ b/src/rules/setsplitting_betweenness.rs @@ -32,8 +32,6 @@ impl ReductionResult for ReductionSetSplittingToBetweenness { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let pole_position = target_solution[self.pole]; Ok(target_solution[..self.source_universe_size] .iter() diff --git a/src/rules/setsplitting_ilp.rs b/src/rules/setsplitting_ilp.rs index db0b8a61c..682e944c0 100644 --- a/src/rules/setsplitting_ilp.rs +++ b/src/rules/setsplitting_ilp.rs @@ -32,8 +32,6 @@ impl ReductionResult for ReductionSetSplittingToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/shortestcommonsupersequence_ilp.rs b/src/rules/shortestcommonsupersequence_ilp.rs index d1c713717..91739dfce 100644 --- a/src/rules/shortestcommonsupersequence_ilp.rs +++ b/src/rules/shortestcommonsupersequence_ilp.rs @@ -31,14 +31,12 @@ impl ReductionResult for ReductionSCSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.max_length, self.alphabet_size + 1, 0, - )? + ) .into_iter() .map(|symbol| (symbol < self.alphabet_size).then_some(symbol)) .collect()) @@ -156,7 +154,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source).expect("reduction should succeed"); let target_config = { diff --git a/src/rules/shortestweightconstrainedpath_ilp.rs b/src/rules/shortestweightconstrainedpath_ilp.rs index fe8ff92be..2fff42466 100644 --- a/src/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/rules/shortestweightconstrainedpath_ilp.rs @@ -44,8 +44,6 @@ impl ReductionResult for ReductionSWCPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ (0..self.num_edges) .map(|edge_idx| { @@ -233,13 +231,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec feasible let source = ShortestWeightConstrainedPath::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![2, 3], vec![1, 2], 0, 2, 4, - ); + ) + .unwrap(); crate::example_db::specs::rule_example_via_ilp::<_, i64>(source) }, }] diff --git a/src/rules/sparsematrixcompression_ilp.rs b/src/rules/sparsematrixcompression_ilp.rs index b19b79368..715acabf1 100644 --- a/src/rules/sparsematrixcompression_ilp.rs +++ b/src/rules/sparsematrixcompression_ilp.rs @@ -26,14 +26,12 @@ impl ReductionResult for ReductionSMCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_rows, self.bound_k, 0, - ) + )) } } @@ -124,7 +122,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source).expect("reduction should succeed"); let ilp_solver = crate::solvers::ILPSolver::new(); diff --git a/src/rules/spinglass_maxcut.rs b/src/rules/spinglass_maxcut.rs index 31e315acc..45d162b27 100644 --- a/src/rules/spinglass_maxcut.rs +++ b/src/rules/spinglass_maxcut.rs @@ -39,8 +39,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&spin| spin == 1).collect()) } } @@ -125,8 +123,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ match self.ancilla { None => target_solution @@ -191,7 +187,12 @@ impl ReduceTo> for SpinGlass { } } - let target = MaxCut::new(SimpleGraph::new(total_vertices, edges), weights); + let target = MaxCut::new( + SimpleGraph::new(total_vertices, edges) + .map_err(>>::target_construction)?, + weights, + ) + .map_err(>>::target_construction)?; Ok(ReductionSGToMaxCut { target, @@ -209,7 +210,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/spinglass_qubo.rs b/src/rules/spinglass_qubo.rs index 1a6a900ad..2e4cb929f 100644 --- a/src/rules/spinglass_qubo.rs +++ b/src/rules/spinglass_qubo.rs @@ -30,8 +30,6 @@ impl ReductionResult for ReductionQUBOToSG { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&spin| spin == 1).collect()) } } @@ -63,10 +61,9 @@ impl ReduceTo> for QUBO { let mut interactions = Vec::new(); let mut onsite = vec![0.0; n]; - for i in 0..n { - for j in i..n { - let q = matrix[i][j]; - if q.abs() < 1e-10 { + for (i, row) in matrix.outer_iterator().enumerate() { + for (j, &q) in row.iter() { + if j < i || q == 0.0 { continue; } @@ -77,7 +74,7 @@ impl ReduceTo> for QUBO { // Off-diagonal: Q_ij * x_i * x_j // J_ij contribution let j_ij = q / 4.0; - if j_ij.abs() > 1e-10 { + if j_ij != 0.0 { interactions.push(((i, j), j_ij)); } // h_i and h_j contributions @@ -121,8 +118,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution .iter() .map(|&bit| if bit { 1 } else { -1 }) @@ -140,7 +135,7 @@ impl ReduceTo> for SpinGlass { fn reduce_to(&self) -> Result { let n = self.num_spins(); - let mut matrix = vec![vec![0.0; n]; n]; + let mut matrix = vec![std::collections::BTreeMap::new(); n]; // Convert using s = 2x - 1: // s_i * s_j = (2x_i - 1)(2x_j - 1) = 4x_i*x_j - 2x_i - 2x_j + 1 @@ -152,19 +147,19 @@ impl ReduceTo> for SpinGlass { // h_i * s_i = h_i * (2x_i - 1) = 2*h_i*x_i - h_i for ((i, j), j_val) in self.interactions() { // Off-diagonal: 4 * J_ij - matrix[i][j] += 4.0 * j_val; + *matrix[i].entry(j).or_insert(0.0) += 4.0 * j_val; // Diagonal contributions: -2 * J_ij - matrix[i][i] -= 2.0 * j_val; - matrix[j][j] -= 2.0 * j_val; + *matrix[i].entry(i).or_insert(0.0) -= 2.0 * j_val; + *matrix[j].entry(j).or_insert(0.0) -= 2.0 * j_val; } // Convert h fields to diagonal for (i, &h) in self.fields().iter().enumerate() { // h_i * s_i -> 2*h_i*x_i - matrix[i][i] += 2.0 * h; + *matrix[i].entry(i).or_insert(0.0) += 2.0 * h; } - let target = QUBO::from_matrix(matrix).map_err(|message| { + let target = QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::, QUBO>( message, ) @@ -184,7 +179,7 @@ impl ReduceTo> for SpinGlass { fn reduce_to(&self) -> Result { let n = self.num_spins(); - let mut matrix = vec![vec![0_i64; n]; n]; + let mut matrix = vec![std::collections::BTreeMap::new(); n]; let overflow = |operation| { crate::rules::ReductionError::integer_overflow::, QUBO>( operation, @@ -195,16 +190,19 @@ impl ReduceTo> for SpinGlass { let interaction = coupling .checked_mul(4) .ok_or_else(|| overflow("scaling a spin-glass interaction"))?; - matrix[i][j] = matrix[i][j] + let coefficient = matrix[i].entry(j).or_insert(0i64); + *coefficient = coefficient .checked_add(interaction) .ok_or_else(|| overflow("summing QUBO interaction coefficients"))?; let diagonal = coupling .checked_mul(2) .ok_or_else(|| overflow("scaling a spin-glass diagonal contribution"))?; - matrix[i][i] = matrix[i][i] + let coefficient = matrix[i].entry(i).or_insert(0i64); + *coefficient = coefficient .checked_sub(diagonal) .ok_or_else(|| overflow("summing QUBO diagonal coefficients"))?; - matrix[j][j] = matrix[j][j] + let coefficient = matrix[j].entry(j).or_insert(0i64); + *coefficient = coefficient .checked_sub(diagonal) .ok_or_else(|| overflow("summing QUBO diagonal coefficients"))?; } @@ -213,14 +211,15 @@ impl ReduceTo> for SpinGlass { let diagonal = field .checked_mul(2) .ok_or_else(|| overflow("scaling a spin-glass field"))?; - matrix[i][i] = matrix[i][i] + let coefficient = matrix[i].entry(i).or_insert(0i64); + *coefficient = coefficient .checked_add(diagonal) .ok_or_else(|| overflow("summing QUBO diagonal coefficients"))?; } Ok(ReductionSGToQUBO { target: - QUBO::from_matrix(matrix).map_err( + QUBO::from_rows(matrix).map_err( crate::rules::ReductionError::construction::< SpinGlass, QUBO, diff --git a/src/rules/stackercrane_ilp.rs b/src/rules/stackercrane_ilp.rs index 5e9a0d7d9..91de9aae0 100644 --- a/src/rules/stackercrane_ilp.rs +++ b/src/rules/stackercrane_ilp.rs @@ -35,11 +35,9 @@ impl ReductionResult for ReductionSCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Decode the permutation: for each position p, find the arc a with x_{a,p} = 1 - one_hot_decode(target_solution, self.num_arcs, self.num_arcs, 0)? + one_hot_decode(target_solution, self.num_arcs, self.num_arcs, 0) }) } } diff --git a/src/rules/steinertree_ilp.rs b/src/rules/steinertree_ilp.rs index e19b19e68..ed5c2d91b 100644 --- a/src/rules/steinertree_ilp.rs +++ b/src/rules/steinertree_ilp.rs @@ -30,14 +30,6 @@ impl ReductionResult for ReductionSteinerTreeToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - if crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)? - .value - .is_none() - { - return Err(crate::rules::ExtractionError::invalid( - "target ILP assignment is infeasible", - )); - } Ok(target_solution[..self.num_edges] .iter() .map(|&value| value == 1) @@ -61,7 +53,7 @@ impl ReduceTo> for SteinerTree { let n = self.num_vertices(); let m = self.num_edges(); let (num_vars, num_constraints) = tree_ilp_sizes(n, m, self.terminals().len())?; - // The source constructor requires at least two distinct terminals. + // The source constructor requires at least one terminal. let root = self.terminals()[0]; let edges = self.graph().edges(); let vertex_var = |v: usize| m + v; @@ -132,7 +124,7 @@ impl ReduceTo> for SteinerTree { } } -/// Bounds for all offsets and allocation sizes; n >= 2 is a source invariant. +/// Bounds for all offsets and allocation sizes; n >= 1 is a source invariant. fn tree_ilp_sizes( n: usize, m: usize, @@ -168,7 +160,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, - num_edges: usize, -} - -impl ReductionResult for ReductionSTIGToILP { - type Source = SteinerTreeInGraphs; - type Target = ILP; - - fn target_problem(&self) -> &ILP { - &self.target - } - - fn extract_solution( - &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - Ok(target_solution[..self.num_edges] - .iter() - .map(|&value| value == 1) - .collect()) - } -} - -#[reduction( - transform = exact { - num_vars = "num_edges + 2 * num_edges * (num_terminals - 1)", - num_constraints = "num_vertices * (num_terminals - 1) + 2 * num_edges * (num_terminals - 1)", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] -impl ReduceTo> for SteinerTreeInGraphs { - type Result = ReductionSTIGToILP; - - fn reduce_to(&self) -> Result { - if self.weights().iter().any(|&weight| weight <= 0) { - return Err(crate::rules::ReductionError::invalid_target::< - SteinerTreeInGraphs, - ILP, - >( - "ILP construction requires strictly positive edge weights" - )); - } - - let n = self.num_vertices(); - let m = self.num_edges(); - let root = *self.terminals().first().ok_or_else(|| { - crate::rules::ReductionError::invalid_target::< - SteinerTreeInGraphs, - ILP, - >("source must contain at least one terminal") - })?; - let non_root_terminals = &self.terminals()[1..]; - let edges = self.graph().edges(); - let num_vars = m + 2 * m * non_root_terminals.len(); - let mut constraints = Vec::new(); - - let edge_var = |edge_idx: usize| edge_idx; - let flow_var = |terminal_pos: usize, edge_idx: usize, dir: usize| -> usize { - m + terminal_pos * 2 * m + 2 * edge_idx + dir - }; - - // Flow conservation for each non-root terminal commodity - for (terminal_pos, &terminal) in non_root_terminals.iter().enumerate() { - for vertex in 0..n { - let mut terms = Vec::new(); - for (edge_idx, &(u, v)) in edges.iter().enumerate() { - if v == vertex { - terms.push((flow_var(terminal_pos, edge_idx, 0), 1)); - terms.push((flow_var(terminal_pos, edge_idx, 1), -1)); - } - if u == vertex { - terms.push((flow_var(terminal_pos, edge_idx, 0), -1)); - terms.push((flow_var(terminal_pos, edge_idx, 1), 1)); - } - } - - let rhs = if vertex == root { - -1 - } else if vertex == terminal { - 1 - } else { - 0 - }; - constraints.push(LinearConstraint::eq(terms, rhs)); - } - } - - // Capacity linking: f^t_{e,dir} <= y_e - for terminal_pos in 0..non_root_terminals.len() { - for edge_idx in 0..m { - let selector = edge_var(edge_idx); - constraints.push(LinearConstraint::le( - vec![(flow_var(terminal_pos, edge_idx, 0), 1), (selector, -1)], - 0, - )); - constraints.push(LinearConstraint::le( - vec![(flow_var(terminal_pos, edge_idx, 1), 1), (selector, -1)], - 0, - )); - } - } - - // Objective: minimize total weight - let edge_weights = self.weights(); - let objective: Vec<(usize, i64)> = edge_weights - .iter() - .enumerate() - .map(|(edge_idx, weight)| (edge_var(edge_idx), weight.to_sum())) - .collect(); - - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; - - Ok(ReductionSTIGToILP { - target, - num_edges: m, - }) - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - vec![crate::example_db::specs::RuleExampleSpec { - id: "steinertreeingraphs_to_ilp", - build: || { - // 4 vertices, 4 edges, 2 terminals - // ILP: 4 + 2*4*1 = 12 binary variables = 4096 configs - let source = SteinerTreeInGraphs::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]), - vec![0, 2], - vec![1, 1, 1, 3], - ); - crate::example_db::specs::rule_example_via_ilp::<_, bool>(source) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/steinertreeingraphs_ilp.rs"] -mod tests; diff --git a/src/rules/stringtostringcorrection_ilp.rs b/src/rules/stringtostringcorrection_ilp.rs index 1f6cf33d2..63a532f40 100644 --- a/src/rules/stringtostringcorrection_ilp.rs +++ b/src/rules/stringtostringcorrection_ilp.rs @@ -58,8 +58,6 @@ impl ReductionResult for ReductionSTSCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.n; let k = self.bound; @@ -88,19 +86,7 @@ impl ReductionResult for ReductionSTSCToILP { .filter(|&j| target_solution[idx_s(n, k, t, j)] == 1) .map(|j| current_len + j), ); - match selected.as_slice() { - [operation] => ops.push(*operation), - [] => { - return Err(crate::rules::ExtractionError::invalid(format!( - "edit step {t} has no selected operation" - ))) - } - _ => { - return Err(crate::rules::ExtractionError::invalid(format!( - "edit step {t} has multiple selected operations" - ))) - } - } + ops.push(selected.into_iter().sum()); } ops }) @@ -395,7 +381,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source).expect("reduction should succeed"); let target_config = { diff --git a/src/rules/strongconnectivityaugmentation_ilp.rs b/src/rules/strongconnectivityaugmentation_ilp.rs index 02bb8a4e3..99d4dab03 100644 --- a/src/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/rules/strongconnectivityaugmentation_ilp.rs @@ -27,8 +27,6 @@ impl ReductionResult for ReductionSCAToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_candidates] .iter() .map(|&value| value == 1) @@ -191,7 +189,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows( + Ok(one_hot_decode_rows( target_solution, self.num_pattern_vertices, self.num_host_vertices, 0, - ) + )) } } @@ -113,8 +111,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, diff --git a/src/rules/subsetsum_closestvectorproblem.rs b/src/rules/subsetsum_closestvectorproblem.rs index dd4762df5..7c3b6136b 100644 --- a/src/rules/subsetsum_closestvectorproblem.rs +++ b/src/rules/subsetsum_closestvectorproblem.rs @@ -3,16 +3,15 @@ use crate::models::algebraic::ClosestVectorProblem; use crate::models::misc::SubsetSum; use crate::reduction; -use crate::registry::ConstructionError; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::types::{Min, Or}; +use num_rational::BigRational; /// Result of reducing SubsetSum to ClosestVectorProblem. #[derive(Debug, Clone)] pub struct ReductionSubsetSumToClosestVectorProblem { target: ClosestVectorProblem, num_elements: usize, - target_distance: f64, } impl ReductionResult for ReductionSubsetSumToClosestVectorProblem { @@ -27,14 +26,6 @@ impl ReductionResult for ReductionSubsetSumToClosestVectorProblem { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let certificate = crate::rules::AggregateReductionResult::extract_value(self, value); - if !certificate.0 { - return Err(crate::rules::ExtractionError::invalid( - "target lattice vector does not certify a subset sum", - )); - } Ok(target_solution[..self.num_elements] .iter() .map(|&value| value == 1) @@ -50,8 +41,8 @@ impl crate::rules::AggregateReductionResult for ReductionSubsetSumToClosestVecto &self.target } - fn extract_value(&self, target_value: Min) -> Or { - Or(target_value == Min(Some(self.target_distance))) + fn extract_value(&self, target_value: Min) -> Or { + Or(target_value == Min(Some(BigRational::from_integer(self.num_elements.into())))) } } @@ -112,9 +103,7 @@ impl ReduceTo> for SubsetSum { } basis.push(column); } - // Carry c_k occurs with +1 in bit k and -2 in bit k-1. Descending - // bit rows and carry columns preserve unit pivots in the formal rank - // checker, without changing its implementation or bypassing validation. + // Carry c_k occurs with +1 in bit k and -2 in bit k-1. for bit in (1..bits).rev() { let mut column = vec![0_i64; rows]; column[rows - 1 - bit] = 1; @@ -126,22 +115,11 @@ impl ReduceTo> for SubsetSum { for bit in 0..bits { target[rows - 1 - bit] = i64::from(self.target().bit(bit as u64)); } - // The checked dense byte count bounds n below 2^30 on 64-bit systems, - // so the integer threshold and its unit squared-distance gap are exact. - let count = >>::exact_i64( - n, - "representing the subset-sum distance threshold", - )?; - let target_distance = crate::types::i64_to_exact_f64(count) - .map_err(ConstructionError::from) - .map_err(>>::target_construction)? - .sqrt(); let target = ClosestVectorProblem::new(basis, target) .map_err(>>::target_construction)?; Ok(ReductionSubsetSumToClosestVectorProblem { target, num_elements: n, - target_distance, }) } } @@ -154,7 +132,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( - SubsetSum::new(vec![3u32, 7, 1, 8], 11u32), + SubsetSum::new(vec![3u32, 7, 1, 8], 11u32).unwrap(), SolutionPair { source_config: serde_json::json!(vec![true, false, false, true]), target_config: serde_json::json!(vec![1, 0, 0, 1, 0, 0, 0]), diff --git a/src/rules/subsetsum_integerexpressionmembership.rs b/src/rules/subsetsum_integerexpressionmembership.rs index c187a66e4..647e72f88 100644 --- a/src/rules/subsetsum_integerexpressionmembership.rs +++ b/src/rules/subsetsum_integerexpressionmembership.rs @@ -21,8 +21,6 @@ impl ReductionResult for ReductionSubsetSumToIntegerExpressionMembership { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Union choice 0 = left = Atom(1) = exclude, choice 1 = right = Atom(s_i+1) = include. // This maps directly to SubsetSum's 0/1 include/exclude encoding. @@ -109,7 +107,8 @@ impl ReduceTo for SubsetSum { })?; Ok(ReductionSubsetSumToIntegerExpressionMembership { - target: IntegerExpressionMembership::new(expr, target), + target: IntegerExpressionMembership::new(expr, target) + .map_err(>::target_construction)?, }) } } @@ -122,7 +121,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( - SubsetSum::new(vec![1u32, 5, 6, 8], 11u32), + SubsetSum::new(vec![1u32, 5, 6, 8], 11u32).unwrap(), SolutionPair { source_config: serde_json::json!(vec![false, true, true, false]), target_config: serde_json::json!(vec![false, true, true, false]), diff --git a/src/rules/subsetsum_integerknapsack.rs b/src/rules/subsetsum_integerknapsack.rs index 1a244f968..de1ebdc90 100644 --- a/src/rules/subsetsum_integerknapsack.rs +++ b/src/rules/subsetsum_integerknapsack.rs @@ -54,7 +54,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let source_bits = &target_solution[..self.source_len]; @@ -109,7 +107,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( - SubsetSum::new(vec![1u32, 5, 6, 8], 11u32), + SubsetSum::new(vec![1u32, 5, 6, 8], 11u32).unwrap(), SolutionPair { source_config: serde_json::json!(vec![false, true, true, false]), target_config: serde_json::json!(vec![false, true, true, false, false]), diff --git a/src/rules/sumofsquarespartition_ilp.rs b/src/rules/sumofsquarespartition_ilp.rs index 1d82db321..469d3bae6 100644 --- a/src/rules/sumofsquarespartition_ilp.rs +++ b/src/rules/sumofsquarespartition_ilp.rs @@ -61,14 +61,12 @@ impl ReductionResult for ReductionSSPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_elements, self.num_groups, 0, - ) + )) } } diff --git a/src/rules/test_helpers.rs b/src/rules/test_helpers.rs index 620f17233..f43dafa45 100644 --- a/src/rules/test_helpers.rs +++ b/src/rules/test_helpers.rs @@ -1,7 +1,7 @@ use crate::rules::{ReductionChain, ReductionResult}; use crate::solvers::BruteForce; +use crate::solvers::SolutionAggregate; use crate::traits::Problem; -use crate::types::SolutionAggregate; use std::collections::HashSet; fn verify_optimization_round_trip( @@ -227,6 +227,7 @@ where R: ReductionResult, R::Source: Problem + 'static, R::Target: Problem> + 'static, + ::Value: SolutionAggregate, ::Value: SolutionAggregate + std::fmt::Debug + PartialEq, { use crate::solvers::ILPSolver; @@ -288,8 +289,12 @@ mod tests { } impl crate::solvers::BruteForceProblem for ToyExtremumProblem { - fn dimensions(&self) -> Vec { - vec![2, 2] + fn num_variables(&self) -> Result { + Ok(2usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([2, 2][variable]) } } @@ -322,8 +327,12 @@ mod tests { } impl crate::solvers::BruteForceProblem for ToyOrProblem { - fn dimensions(&self) -> Vec { - vec![2, 2] + fn num_variables(&self) -> Result { + Ok(2usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([2, 2][variable]) } } @@ -380,8 +389,6 @@ mod tests { target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -403,8 +410,6 @@ mod tests { target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -426,8 +431,6 @@ mod tests { target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -449,8 +452,6 @@ mod tests { target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/threedimensionalmatching_ilp.rs b/src/rules/threedimensionalmatching_ilp.rs index 57db1ddc7..0c544c1c9 100644 --- a/src/rules/threedimensionalmatching_ilp.rs +++ b/src/rules/threedimensionalmatching_ilp.rs @@ -22,8 +22,6 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } @@ -86,7 +84,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/threedimensionalmatching_minimumweightdecoding.rs b/src/rules/threedimensionalmatching_minimumweightdecoding.rs index c8239dae0..1032c8d88 100644 --- a/src/rules/threedimensionalmatching_minimumweightdecoding.rs +++ b/src/rules/threedimensionalmatching_minimumweightdecoding.rs @@ -51,15 +51,6 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToMinimumWeightDecodin &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if target_solution.len() != self.target.num_cols() { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} target codeword bits, got {}", - self.target.num_cols(), - target_solution.len() - ))); - } - Ok(target_solution[..self.source_num_triples].to_vec()) } } @@ -84,7 +75,8 @@ impl ReduceTo for ThreeDimensionalMatching { // q = 0 → Or(true) (empty matching of empty universe) // q ≥ 1 → Or(false) (no triples cannot cover non-empty universe). return Ok(ReductionThreeDimensionalMatchingToMinimumWeightDecoding { - target: MinimumWeightDecoding::new(vec![vec![true]], vec![false]), + target: MinimumWeightDecoding::new(vec![vec![true]], vec![false]) + .map_err(>::target_construction)?, source_num_triples: m, }); } @@ -101,7 +93,8 @@ impl ReduceTo for ThreeDimensionalMatching { let syndrome = vec![true; num_rows]; Ok(ReductionThreeDimensionalMatchingToMinimumWeightDecoding { - target: MinimumWeightDecoding::new(matrix, syndrome), + target: MinimumWeightDecoding::new(matrix, syndrome) + .map_err(>::target_construction)?, source_num_triples: m, }) } @@ -118,7 +111,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( - ThreeDimensionalMatching::new(2, vec![(0, 0, 0), (1, 1, 1), (0, 1, 0), (1, 0, 1)]), + ThreeDimensionalMatching::new(2, vec![(0, 0, 0), (1, 1, 1), (0, 1, 0), (1, 0, 1)]) + .unwrap(), SolutionPair { source_config: serde_json::json!(vec![true, true, false, false]), target_config: serde_json::json!(vec![true, true, false, false]), diff --git a/src/rules/threedimensionalmatching_threepartition.rs b/src/rules/threedimensionalmatching_threepartition.rs index 03a6ce030..c5efede73 100644 --- a/src/rules/threedimensionalmatching_threepartition.rs +++ b/src/rules/threedimensionalmatching_threepartition.rs @@ -9,7 +9,6 @@ use crate::models::misc::ThreePartition; use crate::models::set::ThreeDimensionalMatching; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -use std::collections::HashMap; #[derive(Debug, Clone, Copy)] enum Step2Item { @@ -33,18 +32,6 @@ enum Step2Item { }, } -#[derive(Debug, Clone, Copy)] -enum PairingKind { - U, - UPrime, -} - -#[derive(Debug, Default, Clone, Copy)] -struct PairUsage { - saw_u: bool, - uprime_regulars: Option<[usize; 2]>, -} - /// Result of reducing ThreeDimensionalMatching to ThreePartition. #[derive(Debug, Clone)] pub struct ReductionThreeDimensionalMatchingToThreePartition { @@ -67,27 +54,6 @@ impl ReductionThreeDimensionalMatchingToThreePartition { self.pairing_start() + 2 * self.pair_keys.len() } - fn classify_target_element(&self, element_index: usize) -> TargetElement { - if element_index < self.num_regulars() { - return TargetElement::Regular { - step2_index: element_index, - }; - } - - if element_index < self.filler_start() { - let pairing_offset = element_index - self.pairing_start(); - let pair_index = pairing_offset / 2; - let kind = if pairing_offset.is_multiple_of(2) { - PairingKind::U - } else { - PairingKind::UPrime - }; - return TargetElement::Pairing { pair_index, kind }; - } - - TargetElement::Filler - } - fn decode_real_group(&self, step2_group: [usize; 4]) -> Option { let mut a_item = None; let mut b_item = None; @@ -143,6 +109,8 @@ impl ReductionThreeDimensionalMatchingToThreePartition { #[cfg(test)] fn build_target_witness(&self, source_solution: &[usize]) -> Vec { + use std::collections::HashMap; + let mut a_indices = vec![0usize; self.num_source_triples]; let mut first_b_by_w = HashMap::new(); let mut first_c_by_x = HashMap::new(); @@ -298,98 +266,54 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToThreePartition { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - Ok({ - let mut groups = vec![Vec::new(); self.target.num_groups()]; - for (element_index, &group_index) in target_solution.iter().enumerate() { - groups[group_index].push(element_index); - } - - let mut pair_usage: HashMap<(usize, usize), PairUsage> = HashMap::new(); - - for members in groups.into_iter().filter(|members| !members.is_empty()) { - let mut regulars = Vec::new(); - let mut pairing = None; - let mut has_filler = false; - - for element_index in members { - match self.classify_target_element(element_index) { - TargetElement::Regular { step2_index } => regulars.push(step2_index), - TargetElement::Pairing { pair_index, kind } => { - pairing = Some((pair_index, kind)) - } - TargetElement::Filler => has_filler = true, - } - } - - if has_filler || regulars.len() != 2 { - continue; - } - - let Some((pair_index, kind)) = pairing else { - continue; - }; + let mut groups = vec![Vec::with_capacity(3); self.target.num_groups()]; + let mut positions = Vec::with_capacity(target_solution.len()); + for (element, &group) in target_solution.iter().enumerate() { + positions.push((group, groups[group].len())); + groups[group].push(element); + } - let pair_key = self.pair_keys[pair_index]; - let regular_pair = sorted_pair(regulars[0], regulars[1]); - let usage = pair_usage.entry(pair_key).or_default(); + // Garey--Johnson's reverse construction first normalizes filler triples. + // Each has two pairing elements whose sum equals that of an original + // U/UPrime pair. Exchange the second element with the first's mate: + // they have equal sizes, so both affected triples remain valid. A mate + // cannot belong to an already normalized filler triple unless it is + // already here, so each iteration permanently normalizes one triple. + // Initial index order puts regulars first and fillers last; exchanges + // only move pairing elements, preserving those positions. + let pairing_start = self.pairing_start(); + for filler in self.filler_start()..target_solution.len() { + let (group, _) = positions[filler]; + let first = groups[group][0]; + let second = groups[group][1]; + let mate = pairing_start + ((first - pairing_start) ^ 1); + let (mate_group, mate_slot) = positions[mate]; + groups[group][1] = mate; + groups[mate_group][mate_slot] = second; + positions[mate] = (group, 1); + positions[second] = (mate_group, mate_slot); + } - match kind { - PairingKind::U => { - if regular_pair == [pair_key.0, pair_key.1] { - usage.saw_u = true; - } - } - PairingKind::UPrime => { - usage.uprime_regulars = Some(regular_pair); - } - } + let mut source_solution = vec![false; self.num_source_triples]; + for first in (pairing_start..self.filler_start()).step_by(2) { + let (left, _) = positions[first]; + if groups[left][0] >= pairing_start { + continue; // This complete pair is used by a filler triple. } - - let mut source_solution = vec![false; self.num_source_triples]; - - for ((left, right), usage) in pair_usage { - let Some(other_two) = usage.uprime_regulars else { - continue; - }; - if !usage.saw_u { - continue; - } - - let mut group = [left, right, other_two[0], other_two[1]]; - group.sort_unstable(); - if group.windows(2).any(|window| window[0] == window[1]) { - continue; - } - - if let Some(source_triple) = self.decode_real_group(group) { - source_solution[source_triple] = true; - } + let (right, _) = positions[first + 1]; + // Use the actual regular elements, not the pair's construction + // indices: equal-valued pairing elements are interchangeable. + let regulars = [ + groups[left][0], + groups[left][1], + groups[right][0], + groups[right][1], + ]; + if let Some(source_triple) = self.decode_real_group(regulars) { + source_solution[source_triple] = true; } - - source_solution - }) - } -} - -#[derive(Debug, Clone, Copy)] -enum TargetElement { - Regular { - step2_index: usize, - }, - Pairing { - pair_index: usize, - kind: PairingKind, - }, - Filler, -} - -fn sorted_pair(a: usize, b: usize) -> [usize; 2] { - if a <= b { - [a, b] - } else { - [b, a] + } + Ok(source_solution) } } @@ -655,7 +579,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( - ThreeDimensionalMatching::new(1, vec![(0, 0, 0)]), + ThreeDimensionalMatching::new(1, vec![(0, 0, 0)]).unwrap(), SolutionPair { source_config: serde_json::json!(vec![true]), target_config: serde_json::json!(vec![ diff --git a/src/rules/threepartition_resourceconstrainedscheduling.rs b/src/rules/threepartition_resourceconstrainedscheduling.rs index 817ace389..993b638a2 100644 --- a/src/rules/threepartition_resourceconstrainedscheduling.rs +++ b/src/rules/threepartition_resourceconstrainedscheduling.rs @@ -42,8 +42,6 @@ impl ReductionResult for ReductionThreePartitionToRCS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs b/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs index d1b480304..8c32378bf 100644 --- a/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs +++ b/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs @@ -51,32 +51,19 @@ impl ReductionResult for ReductionThreePartitionToSRTD { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Simulate the schedule to find start times let mut current_time: i64 = 0; let mut slot_assignment = vec![0usize; self.num_element_tasks]; - let slot_width = self.bound.checked_add(1).ok_or_else(|| { - crate::rules::ExtractionError::invalid("slot width overflows i64") - })?; // B + 1 (slot width including the filler gap) + let slot_width = self.bound + 1; for &task in target_solution { let start = current_time.max(self.target.release_times()[task]); - let finish = start - .checked_add(self.target.lengths()[task]) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid("task finish time overflows i64") - })?; - current_time = finish; + current_time = start + self.target.lengths()[task]; // Only element tasks (indices 0..3m) contribute to the partition if task < self.num_element_tasks { - let slot = usize::try_from(start / slot_width).map_err(|_| { - crate::rules::ExtractionError::invalid( - "decoded task slot cannot be represented as usize", - ) - })?; + let slot = (start / slot_width) as usize; slot_assignment[task] = slot; } } @@ -157,7 +144,10 @@ impl ReduceTo for ThreePartition { } Ok(ReductionThreePartitionToSRTD { - target: SequencingWithReleaseTimesAndDeadlines::new(lengths, release_times, deadlines), + target: SequencingWithReleaseTimesAndDeadlines::new(lengths, release_times, deadlines) + .map_err( + >::target_construction, + )?, num_element_tasks: n_elem, bound: b, }) diff --git a/src/rules/timetabledesign_ilp.rs b/src/rules/timetabledesign_ilp.rs index 9e771d48e..e4d795eb8 100644 --- a/src/rules/timetabledesign_ilp.rs +++ b/src/rules/timetabledesign_ilp.rs @@ -35,8 +35,6 @@ impl ReductionResult for ReductionTDToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok((0..self.num_craftsmen) .map(|craftsman| { (0..self.num_tasks) @@ -137,7 +135,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/traits.rs b/src/rules/traits.rs index c33c39754..7fd097e97 100644 --- a/src/rules/traits.rs +++ b/src/rules/traits.rs @@ -160,14 +160,6 @@ impl ExtractionError { pub type ExtractionResult = std::result::Result; -/// Validate a typed target solution and return its evaluated value for reuse. -pub(crate) fn validate_target_solution( - target: &P, - solution: &P::Solution, -) -> ExtractionResult { - Ok(target.evaluate(solution)?) -} - /// Result of reducing a source problem to a target problem. /// /// This trait encapsulates the target problem and provides methods @@ -184,7 +176,9 @@ pub trait ReductionResult { /// Extract a solution from target problem space to source problem space. /// /// # Arguments - /// * `target_solution` - A solution to the target problem + /// * `target_solution` - A target solution satisfying this reduction's + /// mathematical premises, including optimality when required. The solver + /// or external caller establishes these premises before extraction. /// /// # Returns /// The corresponding solution in the source problem space @@ -308,7 +302,6 @@ where } fn extract_solution(&self, target_solution: &T::Solution) -> ExtractionResult { - validate_target_solution(self.target_problem(), target_solution)?; Ok(target_solution.clone()) } } @@ -404,21 +397,13 @@ pub trait DynAggregateReductionResult { 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; - /// Map the value of a target solution without erasing the source value's type. - /// The caller must establish that the solution realizes the target aggregate - /// before interpreting the result as the source aggregate. - fn extract_value_from_solution_dyn( - &self, - target_solution: &dyn Any, - ) -> ExtractionResult>; } impl DynAggregateReductionResult for R where R::Target: 'static, - ::Solution: 'static, ::Value: Serialize + DeserializeOwned, - ::Value: Serialize + 'static, + ::Value: Serialize, { fn target_problem_any(&self) -> &dyn Any { self.target_problem() as &dyn Any @@ -431,22 +416,6 @@ where serde_json::to_value(source_value) .expect("DynAggregateReductionResult source value serialize failed") } - - fn extract_value_from_solution_dyn( - &self, - target_solution: &dyn Any, - ) -> ExtractionResult> { - let target_solution = target_solution - .downcast_ref::<::Solution>() - .ok_or_else(|| { - ExtractionError::invalid(format!( - "target solution type mismatch: expected {}", - std::any::type_name::<::Solution>() - )) - })?; - let target_value = self.target_problem().evaluate(target_solution)?; - Ok(Box::new(self.extract_value(target_value))) - } } #[cfg(test)] diff --git a/src/rules/travelingsalesman_ilp.rs b/src/rules/travelingsalesman_ilp.rs index ee89c2ec7..e1e768c23 100644 --- a/src/rules/travelingsalesman_ilp.rs +++ b/src/rules/travelingsalesman_ilp.rs @@ -36,28 +36,25 @@ impl ReductionResult for ReductionTSPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_vertices; - let tour = one_hot_decode(target_solution, n, n, 0)?; + let tour = one_hot_decode(target_solution, n, n, 0); // Map tour to edge selection let mut edge_selection = vec![false; self.source_edges.len()]; for k in 0..n { let u = tour[k]; let v = tour[(k + 1) % n]; - let edge = self + for (edge, _) in self .source_edges .iter() - .position(|&(a, b)| (a == u && b == v) || (a == v && b == u)) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "target tour uses absent source edge ({u}, {v})" - )) - })?; - edge_selection[edge] = true; + .enumerate() + .filter(|&(_, &(a, b))| (a == u && b == v) || (a == v && b == u)) + .take(1) + { + edge_selection[edge] = true; + } } edge_selection @@ -185,9 +182,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/travelingsalesman_qubo.rs b/src/rules/travelingsalesman_qubo.rs index 42cbda1a9..94801bff6 100644 --- a/src/rules/travelingsalesman_qubo.rs +++ b/src/rules/travelingsalesman_qubo.rs @@ -10,7 +10,7 @@ use crate::models::algebraic::QUBO; use crate::models::graph::TravelingSalesman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -use crate::topology::{Graph, SimpleGraph}; +use crate::topology::SimpleGraph; use std::collections::HashMap; /// Result of reducing TravelingSalesman to QUBO. @@ -20,6 +20,9 @@ pub struct ReductionTravelingSalesmanToQUBO { num_vertices: usize, num_edges: usize, edge_index: HashMap<(usize, usize), usize>, + objective_offset: i128, + feasible_energy_upper: i128, + small_optimum: Option<(Vec, i64)>, } impl ReductionResult for ReductionTravelingSalesmanToQUBO { @@ -30,52 +33,57 @@ impl ReductionResult for ReductionTravelingSalesmanToQUBO { &self.target } - /// Decode position encoding back to edge-based configuration. - /// - /// The QUBO solution uses n^2 binary variables x_{v,p} (vertex v at position p). - /// We extract the tour order, then map consecutive pairs to edge indices. + /// Decode an optimum whose value relation establishes source feasibility. + /// The energy gap guarantees a permutation using existing source edges. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - Ok({ - let n = self.num_vertices; - - let tour: Vec = (0..n) - .map(|position| { - let mut selected = - (0..n).filter(|&vertex| target_solution[vertex * n + position]); - match (selected.next(), selected.next()) { - (Some(vertex), None) => Ok(vertex), - _ => Err(crate::rules::ExtractionError::invalid(format!( - "tour position {position} does not select exactly one vertex" - ))), - } - }) - .collect::>()?; - - // Build edge-based config: for each consecutive pair in the tour, mark the edge - let mut config = vec![false; self.num_edges]; - for p in 0..n { - let u = tour[p]; - let v = tour[(p + 1) % n]; - let key = (u.min(v), u.max(v)); - let &edge = self.edge_index.get(&key).ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "target tour uses absent source edge ({u}, {v})" - )) - })?; - config[edge] = true; - } + if self.num_vertices < 3 { + return Ok(self.small_optimum.as_ref().unwrap().0.clone()); + } + let n = self.num_vertices; + let tour: Vec = (0..n) + .map(|position| { + (0..n) + .find(|&vertex| target_solution[vertex * n + position]) + .unwrap() + }) + .collect(); + let mut config = vec![false; self.num_edges]; + for p in 0..n { + let (u, v) = (tour[p], tour[(p + 1) % n]); + config[self.edge_index[&(u.min(v), u.max(v))]] = true; + } + Ok(config) + } +} - config - }) +impl crate::rules::AggregateReductionResult for ReductionTravelingSalesmanToQUBO { + type Source = TravelingSalesman; + type Target = QUBO; + fn target_problem(&self) -> &Self::Target { + &self.target + } + fn extract_value(&self, value: crate::types::Min) -> crate::types::Min { + if self.num_vertices < 3 { + return crate::types::Min( + value + .0 + .and(self.small_optimum.as_ref().map(|(_, cost)| *cost)), + ); + } + crate::types::Min( + value.0.filter(|&energy| i128::from(energy) < self.feasible_energy_upper) + // Construction bounds source tour costs by a representable sum of + // absolute edge weights; the offset is calculated in i128. + .map(|energy| i64::try_from(i128::from(energy) + self.objective_offset).unwrap()), + ) } } #[reduction( + aggregate = custom, transform = exact { num_vars = "num_vertices^2", } @@ -87,49 +95,106 @@ impl ReduceTo> for TravelingSalesman { let n = self.num_vertices(); let edges = self.edges(); - // Build edge weight map (both directions for undirected lookup) let overflow = |operation| { - crate::rules::ReductionError::integer_overflow::< - TravelingSalesman, - QUBO, - >(operation) + crate::rules::ReductionError::integer_overflow::>(operation) }; - let mut edge_weight_map: HashMap<(usize, usize), i64> = HashMap::new(); - let mut weight_sum = 0i64; - for &(u, v, w) in &edges { - edge_weight_map.insert((u, v), w); - edge_weight_map.insert((v, u), w); - let magnitude = w - .checked_abs() - .ok_or_else(|| overflow("taking the absolute value of a tour weight"))?; - weight_sum = weight_sum - .checked_add(magnitude) - .ok_or_else(|| overflow("summing absolute tour weights"))?; + let num_edges = edges.len(); + let dim = n + .checked_mul(n) + .ok_or_else(|| overflow("computing the number of QUBO variables"))?; + + // The source represents a connected degree-two edge set. With fewer + // than three vertices this means one loop or two parallel edges. + if n < 3 { + let mut candidates: Vec = edges + .iter() + .enumerate() + .filter(|&(_, &(u, v, _))| (n == 1 && u == v) || (n == 2 && u != v)) + .map(|(index, _)| index) + .collect(); + + let small_optimum = if n > 0 && candidates.len() >= n { + candidates.select_nth_unstable_by_key(n - 1, |&index| (edges[index].2, index)); + let mut solution = vec![false; num_edges]; + let mut cost = 0i64; + for &index in &candidates[..n] { + solution[index] = true; + cost = cost + .checked_add(edges[index].2) + .ok_or_else(|| overflow("summing a small tour cost"))?; + } + Some((solution, cost)) + } else { + None + }; + return Ok(ReductionTravelingSalesmanToQUBO { + target: QUBO::from_sparse(sprs::CsMat::zero((dim, dim))) + .map_err(>>::target_construction)?, + num_vertices: n, + num_edges, + edge_index: HashMap::new(), + objective_offset: 0, + feasible_energy_upper: 0, + small_optimum, + }); } - // Build edge index map: canonical (min, max) → edge index - let graph_edges = self.graph().edges(); - let num_edges = graph_edges.len(); + // A tour on at least three vertices uses no loops and at most one + // edge per endpoint pair. Retain the cheapest parallel edge. let mut edge_index: HashMap<(usize, usize), usize> = HashMap::new(); - for (idx, &(u, v)) in graph_edges.iter().enumerate() { - edge_index.insert((u.min(v), u.max(v)), idx); + for (index, &(u, v, weight)) in edges.iter().enumerate() { + if u == v { + continue; + } + let key = (u.min(v), u.max(v)); + edge_index + .entry(key) + .and_modify(|previous| { + if weight < edges[*previous].2 { + *previous = index; + } + }) + .or_insert(index); } - - // Penalty weight: must exceed any possible tour cost - let a = weight_sum + let shift = edge_index + .values() + .map(|&index| edges[index].2) + .fold(0, i64::min); + let mut shifted_sum = 0i64; + let mut absolute_sum = 0i64; + for &index in edge_index.values() { + let weight = edges[index].2; + absolute_sum = absolute_sum + .checked_add( + weight + .checked_abs() + .ok_or_else(|| overflow("taking the absolute value of a tour weight"))?, + ) + .ok_or_else(|| overflow("summing absolute tour weights"))?; + let shifted = weight + .checked_sub(shift) + .ok_or_else(|| overflow("shifting a tour weight"))?; + shifted_sum = shifted_sum + .checked_add(shifted) + .ok_or_else(|| overflow("summing shifted tour weights"))?; + } + // Every permutation tour uses n edges. Shifting each cost therefore + // adds a constant. All costs are now nonnegative even off-premise. + let a = shifted_sum .checked_add(1) .ok_or_else(|| overflow("computing the tour penalty"))?; + let omitted_constant = 2 * n as i128 * i128::from(a); + let objective_offset = omitted_constant + n as i128 * i128::from(shift); + let feasible_energy_upper = i128::from(a) - omitted_constant; // Build n^2 x n^2 upper-triangular QUBO matrix - let dim = n - .checked_mul(n) - .ok_or_else(|| overflow("computing the number of QUBO variables"))?; - let mut matrix = vec![vec![0i64; dim]; dim]; + let mut matrix = vec![std::collections::BTreeMap::new(); dim]; // Helper: add value to upper-triangular position let mut add_upper = |i: usize, j: usize, val: i64| { let (lo, hi) = if i <= j { (i, j) } else { (j, i) }; - matrix[lo][hi] = matrix[lo][hi] + let coefficient = matrix[lo].entry(hi).or_insert(0i64); + *coefficient = coefficient .checked_add(val) .ok_or_else(|| overflow("adding a tour QUBO coefficient"))?; Ok::<(), crate::rules::ReductionError>(()) @@ -189,7 +254,10 @@ impl ReduceTo> for TravelingSalesman { // For each pair (u, v), add cost for x_{u,p} * x_{v,p_next} and x_{v,p} * x_{u,p_next} for u in 0..n { for v in (u + 1)..n { - let cost = edge_weight_map.get(&(u, v)).copied().unwrap_or(a); + let cost = edge_index.get(&(u, v)).map_or(a, |&index| { + // The bound calculation already checked this subtraction. + edges[index].2 - shift + }); for p in 0..n { let p_next = (p + 1) % n; // x_{u,p} * x_{v,p_next} @@ -200,18 +268,17 @@ impl ReduceTo> for TravelingSalesman { } } - let target = QUBO::from_matrix(matrix).map_err(|message| { - crate::rules::ReductionError::construction::< - TravelingSalesman, - QUBO, - >(message) - })?; + let target = + QUBO::from_rows(matrix).map_err(>>::target_construction)?; Ok(ReductionTravelingSalesmanToQUBO { target, num_vertices: n, num_edges, edge_index, + objective_offset, + feasible_energy_upper, + small_optimum: None, }) } } @@ -225,9 +292,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/undirectedflowlowerbounds_ilp.rs b/src/rules/undirectedflowlowerbounds_ilp.rs index 88db49edf..da96a89aa 100644 --- a/src/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/rules/undirectedflowlowerbounds_ilp.rs @@ -58,8 +58,6 @@ impl ReductionResult for ReductionUFLBToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let e = self.num_edges; target_solution[2 * e..3 * e] @@ -185,13 +183,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) }, }] diff --git a/src/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/rules/undirectedtwocommodityintegralflow_ilp.rs index 821af6079..c5feb33c5 100644 --- a/src/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -55,8 +55,6 @@ impl ReductionResult for ReductionU2CIFToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(&target_solution[..4 * self.num_edges]) } } @@ -209,7 +207,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec Vec>::reduce_to(&source).expect("reduction should succeed"); let solver = crate::solvers::ILPSolver::new(); diff --git a/src/rules/unitdiskmapping/ksg/mapping.rs b/src/rules/unitdiskmapping/ksg/mapping.rs index 23e471a58..72dd341eb 100644 --- a/src/rules/unitdiskmapping/ksg/mapping.rs +++ b/src/rules/unitdiskmapping/ksg/mapping.rs @@ -55,7 +55,6 @@ pub struct MappingResult { /// Tape entries recording gadget applications (for unapply during solution extraction). pub tape: Vec, /// Doubled cells (where two copy lines overlap) for map_config_back. - #[serde(default)] pub doubled_cells: HashSet<(usize, usize)>, } @@ -230,16 +229,8 @@ impl MappingResult { &self, grid_config: &[usize], ) -> crate::rules::ExtractionResult> { - self.map_config_back_internal(grid_config) - .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string())) - } - - fn map_config_back_internal( - &self, - grid_config: &[usize], - ) -> Result, ReductionError> { if grid_config.len() != self.positions.len() { - return Err(mapping_invalid( + return Err(crate::rules::ExtractionError::invalid( "grid configuration length must match the mapped vertex count", )); } @@ -248,12 +239,18 @@ impl MappingResult { let mut config_2d = vec![vec![0usize; cols]; rows]; for (idx, &(row, col)) in self.positions.iter().enumerate() { - let row = usize::try_from(row) - .map_err(|_| mapping_invalid("mapping result contains a negative grid row"))?; - let col = usize::try_from(col) - .map_err(|_| mapping_invalid("mapping result contains a negative grid column"))?; + let row = usize::try_from(row).map_err(|_| { + crate::rules::ExtractionError::invalid( + "mapping result contains a negative grid row", + ) + })?; + let col = usize::try_from(col).map_err(|_| { + crate::rules::ExtractionError::invalid( + "mapping result contains a negative grid column", + ) + })?; if row >= rows || col >= cols { - return Err(mapping_invalid( + return Err(crate::rules::ExtractionError::invalid( "mapping result contains a position outside its grid dimensions", )); } @@ -261,7 +258,8 @@ impl MappingResult { } // Step 2: Unapply gadgets in reverse order - unapply_gadgets(&self.tape, &mut config_2d)?; + unapply_gadgets(&self.tape, &mut config_2d) + .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string()))?; // Step 3: Extract vertex configs from copylines map_config_copyback( @@ -271,6 +269,7 @@ impl MappingResult { &config_2d, &self.doubled_cells, ) + .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string())) } } @@ -280,16 +279,8 @@ impl MappingResult { &self, grid_config: &[usize], ) -> crate::rules::ExtractionResult> { - self.map_config_back_internal(grid_config) - .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string())) - } - - fn map_config_back_internal( - &self, - grid_config: &[usize], - ) -> Result, ReductionError> { if grid_config.len() != self.positions.len() { - return Err(mapping_invalid( + return Err(crate::rules::ExtractionError::invalid( "grid configuration length must match the mapped vertex count", )); } @@ -298,12 +289,18 @@ impl MappingResult { let mut config_2d = vec![vec![0usize; cols]; rows]; for (idx, &(row, col)) in self.positions.iter().enumerate() { - let row = usize::try_from(row) - .map_err(|_| mapping_invalid("mapping result contains a negative grid row"))?; - let col = usize::try_from(col) - .map_err(|_| mapping_invalid("mapping result contains a negative grid column"))?; + let row = usize::try_from(row).map_err(|_| { + crate::rules::ExtractionError::invalid( + "mapping result contains a negative grid row", + ) + })?; + let col = usize::try_from(col).map_err(|_| { + crate::rules::ExtractionError::invalid( + "mapping result contains a negative grid column", + ) + })?; if row >= rows || col >= cols { - return Err(mapping_invalid( + return Err(crate::rules::ExtractionError::invalid( "mapping result contains a position outside its grid dimensions", )); } @@ -311,7 +308,8 @@ impl MappingResult { } // Step 2: Unapply gadgets in reverse order - unapply_weighted_gadgets(&self.tape, &mut config_2d)?; + unapply_weighted_gadgets(&self.tape, &mut config_2d) + .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string()))?; // Step 3: Extract vertex configs from copylines map_config_copyback( @@ -321,6 +319,7 @@ impl MappingResult { &config_2d, &self.doubled_cells, ) + .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string())) } } diff --git a/src/rules/unitdiskmapping/triangular/mapping.rs b/src/rules/unitdiskmapping/triangular/mapping.rs index 7fa8ebe29..e662ef529 100644 --- a/src/rules/unitdiskmapping/triangular/mapping.rs +++ b/src/rules/unitdiskmapping/triangular/mapping.rs @@ -310,28 +310,22 @@ pub fn map_config_back( result: &MappingResult, grid_config: &[usize], ) -> crate::rules::ExtractionResult> { - map_config_back_internal(result, grid_config) - .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string())) -} - -fn map_config_back_internal( - result: &MappingResult, - grid_config: &[usize], -) -> Result, ReductionError> { if grid_config.len() != result.positions.len() { - return Err(mapping_invalid( + return Err(crate::rules::ExtractionError::invalid( "grid configuration length must match the mapped vertex count", )); } - let positions = position_index(result)?; + let positions = position_index(result) + .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string()))?; - super::super::weighted::trace_centers(result)? + super::super::weighted::trace_centers(result) + .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string()))? .into_iter() .map(|center| { positions .get(¢er) .map(|&index| grid_config[index]) - .ok_or(mapping_invalid( + .ok_or(crate::rules::ExtractionError::invalid( "a traced center is missing from the mapped graph", )) }) diff --git a/src/solvers/brute_force.rs b/src/solvers/brute_force.rs index fd3add8fe..7ddcdc7e2 100644 --- a/src/solvers/brute_force.rs +++ b/src/solvers/brute_force.rs @@ -4,12 +4,56 @@ use std::any::Any; use crate::solvers::SolveError; use crate::traits::Problem; -use crate::types::{Aggregate, SolutionAggregate}; +use crate::types::{Aggregate, Extremum, Max, Min, Or}; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt; + +/// Brute-force capability for selecting witnesses from a completed aggregate. +/// +/// This is not required by model evaluation, reductions, or solvers that return +/// their solutions directly. +pub trait SolutionAggregate: Aggregate { + /// Whether a solution-level value contributes to the final aggregate value. + fn contributes_to_solution(value: &Self, total: &Self) -> bool; +} + +impl SolutionAggregate + for Max +{ + fn contributes_to_solution(value: &Self, total: &Self) -> bool { + matches!((value, total), (Max(Some(value)), Max(Some(best))) if value == best) + } +} + +impl SolutionAggregate + for Min +{ + fn contributes_to_solution(value: &Self, total: &Self) -> bool { + matches!((value, total), (Min(Some(value)), Min(Some(best))) if value == best) + } +} + +impl SolutionAggregate for Or { + fn contributes_to_solution(value: &Self, total: &Self) -> bool { + value.0 && total.0 + } +} + +impl SolutionAggregate + for Extremum +{ + fn contributes_to_solution(candidate: &Self, total: &Self) -> bool { + matches!( + (candidate.value.as_ref(), total.value.as_ref()), + (Some(value), Some(best)) if candidate.sense == total.sense && value == best + ) + } +} type CartesianWitness

= Option<(

::Solution,

::Value)>; #[doc(hidden)] -pub type BruteForceDimensionsFn = fn(&dyn Any) -> Vec; +pub type BruteForceDimensionsFn = fn(&dyn Any) -> Result, SolveError>; #[doc(hidden)] pub type BruteForceSolveFn = fn(&dyn Any) -> Result, SolveError>; @@ -34,38 +78,43 @@ inventory::collect!(BruteForceRegistration); /// A problem with a finite Cartesian coordinate space for reference solving. pub trait BruteForceProblem: Problem { - /// Cardinality of each coordinate in the brute-force search space. - fn dimensions(&self) -> Vec; + /// Number of coordinates needed to represent one candidate. + fn num_variables(&self) -> Result; + + /// Cardinality of a coordinate. `variable` must be less than `num_variables()`. + fn dimension(&self, variable: usize) -> Result; +} - /// Number of coordinates in the brute-force search space. - fn num_variables(&self) -> usize { - self.dimensions().len() +/// Materialize coordinate cardinalities for enumeration or inspection. +#[doc(hidden)] +pub fn cartesian_dimensions(problem: &P) -> Result, SolveError> { + let count = BruteForceProblem::num_variables(problem)?; + let mut dimensions = Vec::new(); + dimensions.try_reserve_exact(count)?; + for variable in 0..count { + dimensions.push(problem.dimension(variable)?); } + Ok(dimensions) } pub(crate) struct CartesianIndices { dimensions: Vec, current: Option>, - remaining: usize, } impl CartesianIndices { pub(crate) fn new(dimensions: Vec) -> Result { - let total = if dimensions.is_empty() { - 1 - } else if dimensions.contains(&0) { - 0 + let current = if dimensions.contains(&0) { + None } else { - dimensions.iter().try_fold(1usize, |total, &dimension| { - total - .checked_mul(dimension) - .ok_or_else(|| SolveError::SearchSpaceOverflow(dimensions.clone())) - })? + let mut current = Vec::new(); + current.try_reserve_exact(dimensions.len())?; + current.resize(dimensions.len(), 0); + Some(current) }; Ok(Self { - current: (total != 0).then(|| vec![0; dimensions.len()]), dimensions, - remaining: total, + current, }) } } @@ -79,24 +128,23 @@ impl Iterator for CartesianIndices { for index in (0..self.dimensions.len()).rev() { next[index] += 1; if next[index] < self.dimensions[index] { + self.current = Some(next); break; } next[index] = 0; } - self.remaining -= 1; - if self.remaining != 0 { - self.current = Some(next); - } Some(current) } fn size_hint(&self) -> (usize, Option) { - (self.remaining, Some(self.remaining)) + if self.current.is_some() { + (1, None) + } else { + (0, Some(0)) + } } } -impl ExactSizeIterator for CartesianIndices {} - /// Exact reference solver for variants with a registered finite enumeration. #[derive(Debug, Clone, Default)] pub struct BruteForce; @@ -186,7 +234,7 @@ impl BruteForce { F: Fn(Vec) -> P::Solution, { let mut total = P::Value::identity(); - for indices in CartesianIndices::new(problem.dimensions())? { + for indices in CartesianIndices::new(cartesian_dimensions(problem)?)? { total = total.combine(problem.evaluate(&decode(indices))?)?; if total.is_absorbing() { break; @@ -207,7 +255,7 @@ impl BruteForce { { let total = self.solve_cartesian(problem, &decode)?; let mut witnesses = Vec::new(); - for indices in CartesianIndices::new(problem.dimensions())? { + for indices in CartesianIndices::new(cartesian_dimensions(problem)?)? { let solution = decode(indices); let value = problem.evaluate(&solution)?; if P::Value::contributes_to_solution(&value, &total) { @@ -228,7 +276,7 @@ impl BruteForce { F: Fn(Vec) -> P::Solution, { let total = self.solve_cartesian(problem, &decode)?; - for indices in CartesianIndices::new(problem.dimensions())? { + for indices in CartesianIndices::new(cartesian_dimensions(problem)?)? { let solution = decode(indices); let value = problem.evaluate(&solution)?; if P::Value::contributes_to_solution(&value, &total) { diff --git a/src/solvers/customized/closest_vector_problem.rs b/src/solvers/customized/closest_vector_problem.rs index 7f901e8e9..e222cebd2 100644 --- a/src/solvers/customized/closest_vector_problem.rs +++ b/src/solvers/customized/closest_vector_problem.rs @@ -21,23 +21,15 @@ pub(crate) fn solve( .map(|column| { column .iter() - .map(|&entry| { - crate::types::i64_to_exact_f64(entry)?; - Ok(BigRational::from_integer(entry.into())) - }) - .collect::, SolveError>>() + .map(|&entry| BigRational::from_integer(entry.into())) + .collect() }) - .collect::, _>>()?; + .collect::>>(); let target = problem .target() .iter() - .map(|coordinate| { - let value = coordinate.to_f64().map_err(SolveError::Evaluation)?; - BigRational::from_float(value).ok_or_else(|| { - SolveError::NonFiniteResult("converting a CVP target to an exact rational".into()) - }) - }) - .collect::, _>>()?; + .map(ClosestVectorTarget::to_rational) + .collect::>(); let (mu, norms, alpha) = gram_schmidt(&basis, &target); let mut best_squared = (0..n).map(|i| &norms[i] * &alpha[i] * &alpha[i]).sum(); @@ -118,7 +110,6 @@ fn enumerate( center.round().to_integer().to_i64().ok_or_else(|| { SolveError::IntegerOverflow("rounding a CVP enumeration center".into()) })?; - crate::types::i64_to_exact_f64(candidate)?; let nearest = BigRational::from_integer(candidate.into()); let mut step = if center > nearest { 1_i64 } else { -1 }; @@ -127,7 +118,6 @@ fn enumerate( // branch uses the improved incumbent rather than a fixed initial interval. loop { coefficients[level] = candidate; - crate::types::i64_to_exact_f64(candidate)?; let delta = BigRational::from_integer(candidate.into()) - ¢er; let next_squared = &partial_squared + &norms[level] * &delta * δ if next_squared >= *best_squared { @@ -152,9 +142,15 @@ fn enumerate( break; } // Differences +1,-2,+3,... (or -1,+2,-3,...) alternate around the center. - // Exact f64 coefficient transport keeps these i64 updates below 2^55. - candidate += step; - step = -step - step.signum(); + candidate = candidate.checked_add(step).ok_or_else(|| { + SolveError::IntegerOverflow("advancing a CVP enumeration coefficient".into()) + })?; + step = step + .checked_neg() + .and_then(|value| value.checked_sub(step.signum())) + .ok_or_else(|| { + SolveError::IntegerOverflow("advancing a CVP enumeration step".into()) + })?; } Ok(()) } diff --git a/src/solvers/customized/minimum_decision_tree.rs b/src/solvers/customized/minimum_decision_tree.rs index 7422ab1dc..7302195bd 100644 --- a/src/solvers/customized/minimum_decision_tree.rs +++ b/src/solvers/customized/minimum_decision_tree.rs @@ -1,12 +1,23 @@ //! Exact minimum decision tree solver using dynamic programming over object subsets. use crate::models::misc::MinimumDecisionTree; +use crate::solvers::SolveError; -pub(crate) fn solve(problem: &MinimumDecisionTree) -> Option> { +pub(crate) fn solve(problem: &MinimumDecisionTree) -> Result, SolveError> { let n = problem.num_objects(); - let full = (1usize << n) - 1; - let mut costs = vec![usize::MAX; 1usize << n]; - let mut choices = vec![problem.num_tests(); 1usize << n]; + if n >= usize::BITS as usize { + return Err(SolveError::IntegerOverflow( + "indexing object subsets with a usize mask".into(), + )); + } + let states = 1usize << n; + let full = states - 1; + let mut costs = Vec::new(); + costs.try_reserve_exact(states)?; + costs.resize(states, usize::MAX); + let mut choices = Vec::new(); + choices.try_reserve_exact(states)?; + choices.resize(states, problem.num_tests()); for object in 0..n { costs[1 << object] = 0; } @@ -37,9 +48,11 @@ pub(crate) fn solve(problem: &MinimumDecisionTree) -> Option> { } let slots = (1usize << (n - 1)) - 1; - let mut solution = vec![problem.num_tests(); slots]; + let mut solution = Vec::new(); + solution.try_reserve_exact(slots)?; + solution.resize(slots, problem.num_tests()); write_tree(problem, full, 0, &choices, &mut solution); - Some(solution) + Ok(solution) } fn write_tree( diff --git a/src/solvers/customized/shortest_common_superstring.rs b/src/solvers/customized/shortest_common_superstring.rs index bcca60f3b..e3f5a2ea4 100644 --- a/src/solvers/customized/shortest_common_superstring.rs +++ b/src/solvers/customized/shortest_common_superstring.rs @@ -1,8 +1,9 @@ //! Exact shortest common superstring solver using subset dynamic programming. use crate::models::misc::ShortestCommonSuperstring; +use crate::solvers::SolveError; -pub(crate) fn solve(problem: &ShortestCommonSuperstring) -> Option>> { +pub(crate) fn solve(problem: &ShortestCommonSuperstring) -> Result>, SolveError> { let mut strings = problem.strings().to_vec(); strings.sort(); strings.dedup(); @@ -18,17 +19,31 @@ pub(crate) fn solve(problem: &ShortestCommonSuperstring) -> Option>; (1usize << n) * n]; + if n >= usize::BITS as usize { + return Err(SolveError::IntegerOverflow( + "indexing string subsets with a usize mask".into(), + )); + } + let states = 1usize << n; + let cells = states.checked_mul(n).ok_or_else(|| { + SolveError::IntegerOverflow("sizing the superstring dynamic-programming table".into()) + })?; + let mut dp = Vec::>>::new(); + dp.try_reserve_exact(cells)?; + dp.resize(cells, None); for (i, string) in strings.iter().enumerate() { dp[(1 << i) * n + i] = Some(string.clone()); } - for mask in 1usize..(1usize << n) { + for mask in 1usize..states { for last in 0..n { let Some(prefix) = dp[mask * n + last].clone() else { continue; @@ -51,14 +66,14 @@ pub(crate) fn solve(problem: &ShortestCommonSuperstring) -> Option>(); + solution.extend(shortest.into_iter().map(Some)); solution.resize(problem.max_length(), None); - Some(solution) + Ok(solution) } fn contains(haystack: &[usize], needle: &[usize]) -> bool { diff --git a/src/solvers/customized/solver.rs b/src/solvers/customized/solver.rs index 020fd82bc..87b9a5baf 100644 --- a/src/solvers/customized/solver.rs +++ b/src/solvers/customized/solver.rs @@ -70,12 +70,12 @@ register_customized_solver!( register_customized_solver!(GroupingBySwapping, "symbol-block-order", |problem| Ok( super::grouping_by_swapping::solve(problem) )); -register_customized_solver!(ShortestCommonSuperstring, "subset-dp", |problem| Ok( - super::shortest_common_superstring::solve(problem) -)); -register_customized_solver!(MinimumDecisionTree, "subset-dp", |problem| Ok( - super::minimum_decision_tree::solve(problem) -)); +register_customized_solver!(ShortestCommonSuperstring, "subset-dp", |problem| { + super::shortest_common_superstring::solve(problem).map(Some) +}); +register_customized_solver!(MinimumDecisionTree, "subset-dp", |problem| { + super::minimum_decision_tree::solve(problem).map(Some) +}); register_customized_solver!( MinimumCostCirculation, "negative-cycle-canceling", diff --git a/src/solvers/ilp/adapter.rs b/src/solvers/ilp/adapter.rs new file mode 100644 index 000000000..98930f0f2 --- /dev/null +++ b/src/solvers/ilp/adapter.rs @@ -0,0 +1,239 @@ +//! Numerical execution of a native ILP through HiGHS. +//! +//! This module knows only ILP data and backend settings. Registry lookup, +//! type-erased dispatch, and reduction-chain extraction belong to the caller. + +use crate::models::algebraic::{Comparison, ILPCoefficient, ObjectiveSense, VariableDomain, ILP}; +use crate::types::{i64_to_exact_f64, ExactI64ToF64Error, MAX_EXACT_F64_INTEGER}; +use highs::{HighsModelStatus, HighsSolutionStatus, RowProblem, Sense}; + +/// Internal errors are mapped to the existing public solver errors by orchestration. +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub(crate) enum IlpBackendError { + #[error("the ILP is infeasible")] + Infeasible, + #[error("the ILP objective is unbounded")] + Unbounded, + #[error("the ILP solver reached its time limit before proving optimality")] + Timeout, + #[error("the ILP backend failed: {0}")] + BackendFailure(String), + #[error("the ILP backend returned an invalid rounded solution: {0}")] + InvalidSolution(String), + #[error(transparent)] + InexactTransport(#[from] ExactI64ToF64Error), +} + +/// Backend representation is an execution concern, not a model capability. +pub(crate) trait BackendCoefficient: ILPCoefficient { + fn to_backend_number(self) -> Result; +} +impl BackendCoefficient for i64 { + fn to_backend_number(self) -> Result { + Ok(i64_to_exact_f64(self)?) + } +} +impl BackendCoefficient for f64 { + fn to_backend_number(self) -> Result { + Ok(self) + } +} + +fn accept_backend_status(status: HighsModelStatus) -> Result<(), IlpBackendError> { + match status { + HighsModelStatus::Optimal => Ok(()), + HighsModelStatus::Infeasible => Err(IlpBackendError::Infeasible), + HighsModelStatus::Unbounded => Err(IlpBackendError::Unbounded), + HighsModelStatus::ReachedTimeLimit => Err(IlpBackendError::Timeout), + other => Err(IlpBackendError::BackendFailure(format!( + "HiGHS status: {other:?}" + ))), + } +} + +pub(crate) struct HighsAdapter { + time_limit: Option, +} + +impl HighsAdapter { + pub(crate) fn new(time_limit: Option) -> Self { + Self { time_limit } + } + pub(crate) fn solve(&self, problem: &ILP) -> Result, IlpBackendError> + where + V: VariableDomain, + C: BackendCoefficient, + { + if self + .time_limit + .is_some_and(|seconds| !seconds.is_finite() || seconds < 0.0) + { + return Err(IlpBackendError::BackendFailure( + "time limit must be finite and nonnegative".into(), + )); + } + self.solve_with_objective(problem, problem.objective()) + } + + fn solve_with_objective( + &self, + problem: &ILP, + objective_terms: &[(usize, C)], + ) -> Result, IlpBackendError> + where + V: VariableDomain, + C: BackendCoefficient, + { + let n = problem.num_vars(); + if n == 0 { + return if problem + .is_feasible(&[]) + .map_err(|error| IlpBackendError::InvalidSolution(error.to_string()))? + { + Ok(vec![]) + } else { + Err(IlpBackendError::Infeasible) + }; + } + + if n > i32::MAX as usize || problem.constraints().len() > i32::MAX as usize { + return Err(IlpBackendError::BackendFailure( + "ILP dimensions exceed the HiGHS index representation".into(), + )); + } + let mut backend = RowProblem::new(); + let mut costs = vec![0.0; n]; + for &(index, coefficient) in objective_terms { + costs[index] = coefficient.to_backend_number()?; + } + let columns = problem + .variables() + .iter() + .enumerate() + .map(|(index, bounds)| { + let lower = bounds + .lower_bound() + .map(i64_to_exact_f64) + .transpose()? + .unwrap_or(f64::NEG_INFINITY); + let upper = bounds + .upper_bound() + .map(i64_to_exact_f64) + .transpose()? + .unwrap_or(f64::INFINITY); + Ok(backend.add_integer_column(costs[index], lower..=upper)) + }) + .collect::, IlpBackendError>>()?; + let mut terms = Vec::new(); + for constraint in problem.constraints() { + terms.clear(); + for &(index, coefficient) in constraint.terms() { + terms.push((columns[index], coefficient.to_backend_number()?)); + } + let rhs = constraint.rhs().to_backend_number()?; + let (lower, upper) = match constraint.comparison() { + Comparison::Le => (f64::NEG_INFINITY, rhs), + Comparison::Ge => (rhs, f64::INFINITY), + Comparison::Eq => (rhs, rhs), + }; + backend.add_row(lower..=upper, &terms); + } + let sense = match problem.sense() { + ObjectiveSense::Minimize => Sense::Minimise, + ObjectiveSense::Maximize => Sense::Maximise, + }; + let mut model = backend.try_optimise(sense).map_err(|error| { + IlpBackendError::BackendFailure(format!("loading HiGHS model: {error:?}")) + })?; + model.make_quiet(); + for (option, value) in [("random_seed", 0), ("threads", 1)] { + model.try_set_option(option, value).map_err(|error| { + IlpBackendError::BackendFailure(format!("setting {option}: {error:?}")) + })?; + } + for option in ["mip_rel_gap", "mip_abs_gap"] { + model.try_set_option(option, 0.0).map_err(|error| { + IlpBackendError::BackendFailure(format!("setting {option}: {error:?}")) + })?; + } + model.try_set_option("parallel", "off").map_err(|error| { + IlpBackendError::BackendFailure(format!("setting parallel: {error:?}")) + })?; + if let Some(seconds) = self.time_limit { + model + .try_set_option("time_limit", seconds) + .map_err(|error| { + IlpBackendError::BackendFailure(format!("setting time_limit: {error:?}")) + })?; + } + let solved = model.try_solve().map_err(|error| { + IlpBackendError::BackendFailure(format!("running HiGHS: {error:?}")) + })?; + if solved.status() == HighsModelStatus::UnboundedOrInfeasible && !objective_terms.is_empty() + { + // A zero objective cannot be unbounded, so feasibility distinguishes these states. + self.solve_with_objective(problem, &[])?; + return Err(IlpBackendError::Unbounded); + } + accept_backend_status(solved.status())?; + let gap = solved.mip_gap(); + if gap.is_finite() && gap > 0.0 { + return Err(IlpBackendError::BackendFailure(format!( + "HiGHS returned a nonzero optimality gap: {gap}" + ))); + } + if solved.primal_solution_status() != HighsSolutionStatus::Feasible { + return Err(IlpBackendError::BackendFailure( + "HiGHS returned no feasible primal solution".into(), + )); + } + decode_and_validate(problem, solved.get_solution().columns().iter().copied()) + } +} + +fn decode_and_validate( + problem: &ILP, + values: impl IntoIterator, +) -> Result, IlpBackendError> { + let result = values + .into_iter() + .enumerate() + .map(|(index, value)| { + if !value.is_finite() { + return Err(IlpBackendError::InvalidSolution(format!( + "variable {index} is non-finite" + ))); + } + let rounded = value.round(); + if (value - rounded).abs() > 1e-6 { + return Err(IlpBackendError::InvalidSolution(format!( + "variable {index} has non-integral value {value}" + ))); + } + if rounded.abs() > MAX_EXACT_F64_INTEGER as f64 { + return Err(IlpBackendError::InvalidSolution(format!( + "variable {index} value {rounded} exceeds exact f64 integer transport" + ))); + } + Ok(rounded as i64) + }) + .collect::, _>>()?; + if !problem + .is_feasible(&result) + .map_err(|error| IlpBackendError::InvalidSolution(error.to_string()))? + { + return Err(IlpBackendError::InvalidSolution( + "the rounded assignment violates the ILP; this may be caused by numerical tolerances. \ + Consider tightening the backend's integer feasibility tolerance" + .into(), + )); + } + problem + .evaluate_objective(&result) + .map_err(|error| IlpBackendError::InvalidSolution(error.to_string()))?; + Ok(result) +} + +#[cfg(test)] +#[path = "../../unit_tests/solvers/ilp/adapter.rs"] +mod tests; diff --git a/src/solvers/ilp/mod.rs b/src/solvers/ilp/mod.rs index 55556679e..ecf31b7a0 100644 --- a/src/solvers/ilp/mod.rs +++ b/src/solvers/ilp/mod.rs @@ -1,8 +1,8 @@ //! ILP (Integer Linear Programming) solver module. //! -//! This module provides an ILP solver using the HiGHS solver via the `good_lp` crate. -//! It is only available when the `ilp` feature is enabled. +//! This module provides an ILP solver using the HiGHS solver through its native Rust bindings. +pub(super) mod adapter; mod solver; pub use solver::{ILPSolveError, ILPSolver}; diff --git a/src/solvers/ilp/solver.rs b/src/solvers/ilp/solver.rs index e5325a29f..db74766f2 100644 --- a/src/solvers/ilp/solver.rs +++ b/src/solvers/ilp/solver.rs @@ -1,27 +1,16 @@ //! ILP solver implementation using HiGHS. -use crate::models::algebraic::{Comparison, ObjectiveSense, VariableDomain, ILP}; +use super::adapter::{HighsAdapter, IlpBackendError}; use crate::solvers::registry::solver_capability_registry; use crate::solvers::ExactProblemKey; use crate::traits::Problem; -use crate::types::{i64_to_exact_f64, MAX_EXACT_F64_INTEGER}; -use good_lp::highs; -use good_lp::solvers::highs::HighsParallelType; -use good_lp::{ - variable, ProblemVariables, ResolutionError, Solution, SolutionStatus, SolverModel, Variable, -}; /// A failure to produce an ILP solution optimal within backend numerical tolerances. #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] pub enum ILPSolveError { - /// The constraints have no feasible assignment. - #[error("the ILP is infeasible")] + /// The completed solve establishes that the source has no feasible solution. + #[error("the problem is infeasible")] Infeasible, - /// A target witness did not establish the source decision threshold. - #[error( - "the ILP witness does not meet the decision threshold for {0}; the decision is unresolved" - )] - UnresolvedDecision(String), /// The objective is unbounded. #[error("the ILP objective is unbounded")] Unbounded, @@ -32,7 +21,7 @@ pub enum ILPSolveError { #[error("the ILP backend failed: {0}")] BackendFailure(String), /// Type-erased dispatch received a value other than a supported ILP variant. - #[error("the ILP backend requires bool/i64 variables and f64 coefficients")] + #[error("the ILP backend requires bool/i64 variables and i64/f64 coefficients")] UnsupportedProblemType, /// No ILP pipeline is registered for the exact problem variant. #[error("no ILP pipeline is registered for {0}")] @@ -43,9 +32,12 @@ pub enum ILPSolveError { /// A registered pipeline returned a solution for a different source type. #[error("registered ILP pipeline returned the wrong solution type for {0}")] PipelineTypeMismatch(String), - /// HiGHS reported an optimal solution that is invalid after integer rounding. - #[error("the ILP backend returned an invalid rounded solution: {0}")] + /// The backend or reduction pipeline returned an invalid witness. + #[error("the ILP solve returned an invalid solution: {0}")] InvalidSolution(String), + /// Evaluating the extracted source witness failed. + #[error(transparent)] + Evaluation(#[from] crate::traits::EvaluationError), /// An exact integer in the model cannot be transported through the f64 backend API. #[error("the ILP backend cannot represent an exact model integer: {0}")] InexactTransport(#[from] crate::types::ExactI64ToF64Error), @@ -57,19 +49,24 @@ pub enum ILPSolveError { Reduction(#[from] crate::rules::ReductionError), } -fn classify_backend_error(error: ResolutionError, time_limit: Option) -> ILPSolveError { - match error { - ResolutionError::Infeasible => ILPSolveError::Infeasible, - ResolutionError::Unbounded => ILPSolveError::Unbounded, - ResolutionError::Other("NoSolutionFound") if time_limit.is_some() => ILPSolveError::Timeout, - other => ILPSolveError::BackendFailure(other.to_string()), +// Keep adapter details out of the public error vocabulary. +impl From for ILPSolveError { + fn from(error: IlpBackendError) -> Self { + match error { + IlpBackendError::Infeasible => Self::Infeasible, + IlpBackendError::Unbounded => Self::Unbounded, + IlpBackendError::Timeout => Self::Timeout, + IlpBackendError::BackendFailure(message) => Self::BackendFailure(message), + IlpBackendError::InvalidSolution(message) => Self::InvalidSolution(message), + IlpBackendError::InexactTransport(error) => Self::InexactTransport(error), + } } } /// An ILP solver using the HiGHS backend. /// -/// Registered reductions map a source problem to an `ILP` terminal, -/// which this solver sends to HiGHS before extracting the source solution. +/// Registered reductions map a source problem to its native `ILP` terminal. +/// A shared adapter sends that ILP to HiGHS before source solution extraction. /// Optimality and infeasibility are assessed within HiGHS numerical tolerances. /// Zero MIP gaps do not make floating-point solving mathematically exact. /// @@ -127,172 +124,7 @@ impl ILPSolver { .lookup(&key) .ilp .ok_or_else(|| ILPSolveError::MissingPipeline(key.label()))?; - pipeline.solve_typed(problem, self) - } - - fn solve_backend(&self, problem: &ILP) -> Result, ILPSolveError> - where - V: VariableDomain, - { - self.solve_with_objective(problem, problem.objective()) - } - - fn solve_with_objective( - &self, - problem: &ILP, - objective_terms: &[(usize, f64)], - ) -> Result, ILPSolveError> - where - V: VariableDomain, - { - let n = problem.num_vars(); - if n == 0 { - return if problem - .is_feasible(&[]) - .map_err(|error| ILPSolveError::InvalidSolution(error.to_string()))? - { - Ok(vec![]) - } else { - Err(ILPSolveError::Infeasible) - }; - } - - let mut vars_builder = ProblemVariables::new(); - let vars: Vec = problem - .variables() - .iter() - .map(|variable_bounds| { - let mut definition = variable().integer(); - if let Some(lower) = variable_bounds.lower_bound() { - definition = definition.min(i64_to_exact_f64(lower)?); - } - if let Some(upper) = variable_bounds.upper_bound() { - definition = definition.max(i64_to_exact_f64(upper)?); - } - Ok(vars_builder.add(definition)) - }) - .collect::>()?; - - // Build objective expression - let objective: good_lp::Expression = objective_terms - .iter() - .map(|&(var_idx, coefficient)| coefficient * vars[var_idx]) - .sum(); - - // Build the model with objective - let unsolved = match problem.sense() { - ObjectiveSense::Maximize => vars_builder.maximise(&objective), - ObjectiveSense::Minimize => vars_builder.minimise(&objective), - }; - - // Create the solver model - let mut model = { - let mut model = unsolved - .using(highs) - .set_option("random_seed", 0i32) - .set_option("mip_rel_gap", 0.0) - .set_option("mip_abs_gap", 0.0) - .set_parallel(HighsParallelType::Off) - .set_threads(1); - if let Some(seconds) = self.time_limit { - model = model.set_time_limit(seconds); - } - model - }; - - // Add constraints - for constraint in problem.constraints() { - // Build left-hand side expression - let lhs: good_lp::Expression = constraint - .terms() - .iter() - .map(|&(var_idx, coefficient)| coefficient * vars[var_idx]) - .sum(); - - let rhs = constraint.rhs(); - - // Create the constraint based on comparison type - let good_lp_constraint = match constraint.comparison() { - Comparison::Le => lhs.leq(rhs), - Comparison::Ge => lhs.geq(rhs), - Comparison::Eq => lhs.eq(rhs), - }; - - model = model.with(good_lp_constraint); - } - - // Solve - let solution = match model.solve() { - Ok(solution) => solution, - Err(ResolutionError::Infeasible) - if !objective_terms.is_empty() - && problem.variables().iter().any(|variable| { - variable.lower_bound().is_none() || variable.upper_bound().is_none() - }) => - { - // A zero objective cannot be unbounded, so feasibility distinguishes the two states. - self.solve_with_objective(problem, &[])?; - return Err(ILPSolveError::Unbounded); - } - Err(error) => return Err(classify_backend_error(error, self.time_limit)), - }; - - match solution.status() { - SolutionStatus::Optimal => {} - SolutionStatus::TimeLimit => return Err(ILPSolveError::Timeout), - SolutionStatus::GapLimit => { - return Err(ILPSolveError::BackendFailure( - "the backend stopped at its gap limit before proving optimality".to_string(), - )); - } - } - - let result: Vec = vars - .iter() - .enumerate() - .map(|(index, v)| { - let value = solution.value(*v); - if !value.is_finite() { - return Err(ILPSolveError::InvalidSolution(format!( - "variable {index} is non-finite" - ))); - } - let rounded = value.round(); - if (value - rounded).abs() > 1e-6 { - return Err(ILPSolveError::InvalidSolution(format!( - "variable {index} has non-integral value {value}" - ))); - } - if rounded.abs() > MAX_EXACT_F64_INTEGER as f64 { - return Err(ILPSolveError::InvalidSolution(format!( - "variable {index} value {rounded} exceeds exact f64 integer transport" - ))); - } - Ok(rounded as i64) - }) - .collect::>()?; - - if !problem - .is_feasible(&result) - .map_err(|error| ILPSolveError::InvalidSolution(error.to_string()))? - { - return Err(ILPSolveError::InvalidSolution( - "the rounded assignment violates the ILP".into(), - )); - } - - Ok(result) - } - - /// Solve a type-erased supported ILP variant directly. - pub(crate) fn solve_dyn(&self, any: &dyn std::any::Any) -> Result, ILPSolveError> { - if let Some(ilp) = any.downcast_ref::>() { - return self.solve_backend(ilp); - } - if let Some(ilp) = any.downcast_ref::>() { - return self.solve_backend(ilp); - } - Err(ILPSolveError::UnsupportedProblemType) + pipeline.solve_typed(problem, &HighsAdapter::new(self.time_limit)) } } diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index a0e39a160..47351bb48 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -11,18 +11,22 @@ pub mod ilp; #[doc(hidden)] pub use brute_force::BruteForceRegistration; -pub use brute_force::{BruteForce, BruteForceProblem}; +pub use brute_force::{cartesian_dimensions, BruteForce, BruteForceProblem, SolutionAggregate}; pub use registry::{ brute_force_dimensions, solver_capabilities, CustomizedSolverCapability, ExactProblemKey, IlpSolverCapability, RegistryBuildError, SolverCapabilities, }; -pub use resolver::{solve, SolveOutcome, SolveResult, SolverExecution, SolverRequest}; +pub use resolver::{ + complete_reduction, solve, SolveOutcome, SolveResult, SolverExecution, SolverRequest, +}; pub use ilp::{ILPSolveError, ILPSolver}; /// Failure while solving a valid problem instance. #[derive(Debug, thiserror::Error)] pub enum SolveError { + #[error(transparent)] + Extraction(#[from] crate::rules::ExtractionError), #[error("configuration evaluation failed: {0}")] Evaluation(#[from] crate::traits::EvaluationError), #[error("aggregate combination failed: {0}")] @@ -31,8 +35,8 @@ pub enum SolveError { MissingRegistration(String), #[error("invalid reference-solver registration: {0}")] RegistrationTypeMismatch(String), - #[error("brute-force search space cardinality exceeds usize for dimensions {0:?}")] - SearchSpaceOverflow(Vec), + #[error("cannot allocate solver storage: {0}")] + Allocation(#[from] std::collections::TryReserveError), #[error("integer overflow while {0}")] IntegerOverflow(String), #[error("inexact integer-to-float conversion: {0}")] @@ -52,3 +56,9 @@ pub enum SolveError { source: ILPSolveError, }, } + +impl From for SolveError { + fn from(error: std::num::TryFromIntError) -> Self { + Self::IntegerOverflow(error.to_string()) + } +} diff --git a/src/solvers/pipelines.rs b/src/solvers/pipelines.rs index 15e5aa525..d9674cd78 100644 --- a/src/solvers/pipelines.rs +++ b/src/solvers/pipelines.rs @@ -23,12 +23,10 @@ macro_rules! register_ilp_pipeline { register_ilp_pipeline! { ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -42,118 +40,99 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("AcyclicPartition", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BMF", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BalancedCompleteBipartiteSubgraph", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BicliqueCover", []), ("BMF", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BiconnectivityAugmentation", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BinPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BottleneckTravelingSalesman", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BoundedComponentSpanningForest", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("CapacityAssignment", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("CircuitSAT", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ClosestString", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ClosestSubstring", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Clustering", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ConsecutiveBlockMinimization", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ConsecutiveOnesMatrixAugmentation", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ConsecutiveOnesSubmatrix", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ConsistencyOfDatabaseFrequencyTables", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DecisionMinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "One")]), ("MinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DecisionMinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -161,50 +140,42 @@ register_ilp_pipeline! { ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumSetCovering", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DecisionOptimalLinearArrangement", [("graph", "SimpleGraph")]), ("OptimalLinearArrangement", [("graph", "SimpleGraph")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DirectedHamiltonianPath", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DirectedTwoCommodityIntegralFlow", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DisjointConnectingPaths", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("EnsembleComputation", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("EulerianPath", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ExactCoverBy3Sets", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -215,44 +186,37 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("Factoring", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("FeasibleRegisterAssignment", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("FlowShopScheduling", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("GraphPartitioning", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("HamiltonianCircuit", [("graph", "SimpleGraph")]), ("LongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("HamiltonianPath", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("HighlyConnectedDeletion", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } // This exact variant also has a customized backend. Default dispatch selects the @@ -261,50 +225,42 @@ register_ilp_pipeline! { ("RootedTreeArrangement", [("graph", "SimpleGraph")]), ("RootedTreeStorageAssignment", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("IntegralFlowBundles", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("IntegralFlowHomologousArcs", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("IntegralFlowWithMultipliers", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("IsomorphicSpanningTree", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("KClique", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("KColoring", [("graph", "SimpleGraph"), ("k", "KN")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("KColoring", [("graph", "SimpleGraph"), ("k", "K3")]), ("Clustering", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -312,49 +268,41 @@ register_ilp_pipeline! { ("Satisfiability", []), ("NAESatisfiability", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Knapsack", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("LengthBoundedDisjointPaths", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("LongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("LongestCommonSubsequence", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("LongestPath", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximalIS", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Maximum2Satisfiability", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -363,43 +311,36 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumCoKPlex", [("graph", "SimpleGraph"), ("k", "KN"), ("weight", "One")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumCoKPlex", [("graph", "SimpleGraph"), ("k", "KN"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumCommonEdgeSubgraph", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumContactMapOverlap", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumDomaticNumber", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -410,7 +351,6 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("MaximumEdgeWeightedKClique", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -418,7 +358,6 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -428,14 +367,12 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -444,7 +381,6 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -453,7 +389,6 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -462,7 +397,6 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -470,32 +404,27 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumLeafSpanningTree", [("graph", "SimpleGraph")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumLikelihoodRanking", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumMatching", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumSetPacking", [("weight", "One")]), ("MaximumSetPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -507,31 +436,26 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("MaximumSetPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinMaxMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumCapacitatedSpanningTree", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumCoveringByCliques", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumCutIntoBoundedSets", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -543,245 +467,205 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("MinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumEdgeCostFlow", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumExternalMacroDataCompression", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumFaultDetectionTestSet", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumFeedbackArcSet", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumFeedbackVertexSet", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumGraphBandwidth", [("graph", "SimpleGraph")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumHittingSet", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumInternalMacroDataCompression", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumMatrixCover", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumMaximalMatching", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumMetricDimension", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumMultiwayCut", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumSetCovering", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumTardinessSequencing", [("weight", "One")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumTardinessSequencing", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "One")]), ("MinimumHittingSet", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumSetCovering", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumWeightDecoding", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MixedChinesePostman", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MonochromaticTriangle", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MultipleCopyFileAllocation", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MultipleChoiceBranching", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MultiprocessorScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("NAESatisfiability", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Numerical3DimensionalMatching", []), ("NumericalMatchingWithTargetSums", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("NumericalMatchingWithTargetSums", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("OpenShopScheduling", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("OptimalLinearArrangement", [("graph", "SimpleGraph")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("OptimumCommunicationSpanningTree", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PaintShop", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PartiallyOrderedKnapsack", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Partition", []), ("MultiprocessorScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PartitionIntoCliques", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PartitionIntoPathsOfLength2", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PartitionIntoTriangles", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PathConstrainedNetworkFlow", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PrecedenceConstrainedScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PreemptiveScheduling", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -792,122 +676,102 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("QUBO", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("QuadraticAssignment", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("RectilinearPictureCompression", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("RegisterSufficiency", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ResourceConstrainedScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("RootedTreeStorageAssignment", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("RuralPostman", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Satisfiability", []), ("NAESatisfiability", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SchedulingToMinimizeWeightedCompletionTime", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SchedulingWithIndividualDeadlines", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingToMinimizeMaximumCumulativeCost", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingToMinimizeTardyTaskWeight", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingToMinimizeWeightedTardiness", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingWithDeadlinesAndSetUpTimes", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingWithReleaseTimesAndDeadlines", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingWithinIntervals", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SetSplitting", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ShortestCommonSupersequence", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ShortestWeightConstrainedPath", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SparseMatrixCompression", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -920,66 +784,55 @@ register_ilp_pipeline! { ("SpinGlass", [("graph", "SimpleGraph"), ("weight", "i64")]), ("QUBO", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("StackerCrane", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("StringToStringCorrection", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("StrongConnectivityAugmentation", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SubgraphIsomorphism", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SumOfSquaresPartition", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ThreeDimensionalMatching", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ThreePartition", []), ("ResourceConstrainedScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("TravelingSalesman", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("UndirectedFlowLowerBounds", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("UndirectedTwoCommodityIntegralFlow", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } diff --git a/src/solvers/registry.rs b/src/solvers/registry.rs index c044e3d36..2619ba8c5 100644 --- a/src/solvers/registry.rs +++ b/src/solvers/registry.rs @@ -1,13 +1,31 @@ //! Deterministic solver capabilities for exact problem variants. +use super::ilp::adapter::HighsAdapter; +use crate::models::algebraic::ILP; use crate::registry::VariantEntry; -use crate::rules::registry::{reduction_entries, AggregateReduceFn, ReduceFn, ReductionEntry}; +use crate::rules::registry::{reduction_entries, ReduceFn, ReductionEntry}; use crate::rules::DynReductionResult; use serde::Serialize; use std::any::Any; use std::collections::{BTreeMap, BTreeSet}; use std::sync::OnceLock; +/// Type erasure is resolved at the registry boundary, never inside the adapter. +fn solve_ilp_terminal( + source: &dyn Any, + adapter: &HighsAdapter, +) -> Result, super::ILPSolveError> { + macro_rules! dispatch { + ($($v:ty, $c:ty);* $(;)?) => { $( + if let Some(ilp) = source.downcast_ref::>() { + return adapter.solve(ilp).map_err(Into::into); + } + )* }; + } + dispatch! { bool, i64; i64, i64; bool, f64; i64, f64; } + Err(super::ILPSolveError::UnsupportedProblemType) +} + /// Canonical identity of one concrete problem variant. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)] pub struct ExactProblemKey { @@ -53,7 +71,10 @@ impl ExactProblemKey { self.variant.get("variable").map(String::as_str), Some("bool" | "i64") ) - && self.variant.get("coefficient").map(String::as_str) == Some("f64") + && matches!( + self.variant.get("coefficient").map(String::as_str), + Some("i64" | "f64") + ) } } @@ -103,7 +124,7 @@ inventory::collect!(CustomizedSolverRegistration); #[derive(Debug)] pub(crate) struct CompiledIlpPipeline { path: Vec, - reducers: Vec<(ReduceFn, Option)>, + reducers: Vec, } impl CompiledIlpPipeline { @@ -118,59 +139,29 @@ impl CompiledIlpPipeline { fn solve_with( &self, source: &dyn Any, - solver: &super::ILPSolver, + adapter: &HighsAdapter, finish: impl FnOnce( Box, Option<&dyn DynReductionResult>, ) -> Result, ) -> Result { if self.reducers.is_empty() { - return finish(Box::new(solver.solve_dyn(source)?), None); + return finish(Box::new(solve_ilp_terminal(source, adapter)?), None); } - let mut reductions: Vec> = Vec::new(); - for (reducer, _) in &self.reducers { - let input = reductions - .last() - .map(|step| step.target_problem_any()) - .unwrap_or(source); - reductions.push(reducer(input)?); - } - - let target = reductions - .last() - .expect("non-empty fixed pipeline must produce a target") - .target_problem_any(); - let solution = solver.solve_dyn(target)?; - let mut source_solution: Box = Box::new(solution); - for (index, step) in reductions.iter().enumerate().rev() { - if let Some(reduce) = self.reducers[index].1 { - let input = if index == 0 { - source - } else { - reductions[index - 1].target_problem_any() - }; - let aggregate = reduce(input)?; - // A numerical target optimum can establish YES through a source witness, - // but a missed threshold alone cannot establish NO. - let value = aggregate.extract_value_from_solution_dyn(source_solution.as_ref())?; - if value.downcast_ref::() == Some(&crate::types::Or(false)) { - return Err(super::ILPSolveError::UnresolvedDecision( - self.path[index].label(), - )); - } - } - source_solution = step.extract_solution_dyn(source_solution.as_ref())?; - } - finish(source_solution, Some(reductions[0].as_ref())) + let chain = crate::rules::ReductionChain::execute(source, &self.reducers)?; + let target_solution = solve_ilp_terminal(chain.target_problem_any(), adapter)?; + let source_solution = super::resolver::complete_chain(&chain, &target_solution)? + .ok_or(super::ILPSolveError::Infeasible)?; + finish(source_solution, Some(chain.steps[0].witness.as_ref())) } pub(crate) fn solve( &self, source: &dyn Any, - solver: &super::ILPSolver, + adapter: &HighsAdapter, ) -> Result { - self.solve_with(source, solver, |solution, first_reduction| { + self.solve_with(source, adapter, |solution, first_reduction| { if let Some(reduction) = first_reduction { return reduction .source_solution_json(solution.as_ref()) @@ -185,23 +176,20 @@ impl CompiledIlpPipeline { }) } - pub(crate) fn solve_typed( + pub(crate) fn solve_typed

( &self, - source: &dyn Any, - solver: &super::ILPSolver, - ) -> Result { - self.solve_with(source, solver, |solution, _| { - solution - .downcast::() - .map(|solution| *solution) - .map_err(|_| { - super::ILPSolveError::PipelineTypeMismatch( - self.path - .first() - .expect("compiled pipeline has a source") - .label(), - ) - }) + source: &P, + adapter: &HighsAdapter, + ) -> Result + where + P: crate::traits::Problem + 'static, + P::Solution: 'static, + { + self.solve_with(source, adapter, |solution, _| { + let solution = solution + .downcast::() + .map_err(|_| super::ILPSolveError::PipelineTypeMismatch(self.path[0].label()))?; + Ok(*solution) }) } } @@ -287,7 +275,7 @@ pub enum RegistryBuildError { MissingSolverCapability(String), #[error("ILP pipeline must contain at least one node")] EmptyPipeline, - #[error("ILP pipeline for {0} does not end at an f64-coefficient ILP")] + #[error("ILP pipeline for {0} does not end at a supported native ILP")] UnsupportedTarget(String), #[error("ILP pipeline for {0} continues after reaching a supported ILP node")] ContinuesAfterIlp(String), @@ -411,12 +399,11 @@ fn build_registry( matches: matches.len(), }); } - reducers.push(( + reducers.push( matches[0] .reduce_fn .expect("indexed only entries with reduce_fn"), - matches[0].reduce_aggregate_fn, - )); + ); } if registry @@ -485,10 +472,12 @@ pub(crate) fn brute_force_registration( #[doc(hidden)] pub fn brute_force_dimensions( problem: &crate::registry::LoadedDynProblem, -) -> Result>, &'static RegistryBuildError> { +) -> Result>, crate::solvers::SolveError> { let key = ExactProblemKey::new(problem.problem_name(), problem.variant_map()); - Ok(brute_force_registration(&key)? - .map(|registration| (registration.dimensions_fn)(problem.as_any()))) + brute_force_registration(&key) + .map_err(crate::solvers::SolveError::InvalidRegistry)? + .map(|registration| (registration.dimensions_fn)(problem.as_any())) + .transpose() } #[cfg(test)] diff --git a/src/solvers/resolver.rs b/src/solvers/resolver.rs index 6444d1254..eadfd5e75 100644 --- a/src/solvers/resolver.rs +++ b/src/solvers/resolver.rs @@ -45,6 +45,51 @@ pub enum SolveOutcome { Infeasible, } +/// Interpret aggregate outcomes before mapping each accepted target optimum. +pub(crate) fn complete_chain( + chain: &crate::rules::ReductionChain, + target_solution: &dyn std::any::Any, +) -> crate::rules::ExtractionResult>> { + let mut solution: Option> = None; + for step in chain.steps.iter().rev() { + let input = solution.as_deref().unwrap_or(target_solution); + if let Some(interpret) = &step.interpret_optimum { + if !interpret(input)? { + return Ok(None); + } + } + solution = Some(step.witness.extract_solution_dyn(input)?); + } + Ok(Some(solution.expect("reduction chain has no steps"))) +} + +/// Map a completed target solve through an executed reduction chain. +/// +/// The target outcome must come from a completed solve, not merely a feasible +/// assignment: only an accepted optimum can establish a source decision's NO. +pub fn complete_reduction( + source: &dyn crate::registry::DynProblem, + chain: &crate::rules::ReductionChain, + target: &SolveOutcome, +) -> Result { + let SolveOutcome::Optimal { solution, .. } = target else { + return Ok(SolveOutcome::Infeasible); + }; + let last = chain.steps.last().expect("reduction chain has no steps"); + let target_solution = last.witness.target_solution_from_json(solution.clone())?; + let Some(solution) = complete_chain(chain, target_solution.as_ref())? else { + return Ok(SolveOutcome::Infeasible); + }; + let solution = chain.steps[0] + .witness + .source_solution_json(solution.as_ref())?; + let (evaluation, _) = source.evaluate_dyn(&solution)?; + Ok(SolveOutcome::Optimal { + solution, + evaluation, + }) +} + fn problem_key(problem: &LoadedDynProblem) -> ExactProblemKey { ExactProblemKey::new(problem.problem_name(), problem.variant_map()) } @@ -54,10 +99,13 @@ fn solve_customized( registration: &'static CustomizedSolverRegistration, ) -> Result { let outcome = match (registration.solve_fn)(problem.as_any())? { - Some(solution) => SolveOutcome::Optimal { - evaluation: problem.evaluate_dyn(&solution)?, - solution, - }, + Some(solution) => { + let (evaluation, _) = problem.evaluate_dyn(&solution)?; + SolveOutcome::Optimal { + evaluation, + solution, + } + } None => SolveOutcome::Infeasible, }; Ok(SolveResult { @@ -72,11 +120,17 @@ fn solve_ilp( problem: &LoadedDynProblem, pipeline: &CompiledIlpPipeline, ) -> Result { - let outcome = match pipeline.solve(problem.as_any(), &super::ILPSolver::new()) { - Ok(solution) => SolveOutcome::Optimal { - evaluation: problem.evaluate_dyn(&solution)?, - solution, - }, + let outcome = match pipeline.solve( + problem.as_any(), + &super::ilp::adapter::HighsAdapter::new(None), + ) { + Ok(solution) => { + let (evaluation, _) = problem.evaluate_dyn(&solution)?; + SolveOutcome::Optimal { + evaluation, + solution, + } + } Err(super::ILPSolveError::Infeasible) => SolveOutcome::Infeasible, Err(source) => { return Err(super::SolveError::IlpSolve { diff --git a/src/topology/bipartite_graph.rs b/src/topology/bipartite_graph.rs index a99d5dbdf..e1f91969f 100644 --- a/src/topology/bipartite_graph.rs +++ b/src/topology/bipartite_graph.rs @@ -16,12 +16,13 @@ use serde::{Deserialize, Serialize}; /// use problemreductions::topology::{BipartiteGraph, Graph}; /// /// // K_{2,2}: complete bipartite graph -/// let g = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]); +/// let g = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]).unwrap(); /// assert_eq!(g.num_vertices(), 4); /// assert_eq!(g.num_edges(), 4); /// assert!(g.has_edge(0, 2)); // left 0 -> right 0 (unified index 2) /// ``` #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "BipartiteGraphData")] pub struct BipartiteGraph { left_size: usize, right_size: usize, @@ -29,6 +30,20 @@ pub struct BipartiteGraph { edges: Vec<(usize, usize)>, } +#[derive(Deserialize)] +struct BipartiteGraphData { + left_size: usize, + right_size: usize, + edges: Vec<(usize, usize)>, +} + +impl TryFrom for BipartiteGraph { + type Error = crate::registry::ConstructionError; + fn try_from(data: BipartiteGraphData) -> Result { + Self::new(data.left_size, data.right_size, data.edges) + } +} + impl BipartiteGraph { /// Create a new bipartite graph. /// @@ -38,29 +53,36 @@ impl BipartiteGraph { /// * `right_size` - Number of vertices in the right partition /// * `edges` - Edges as `(left_index, right_index)` pairs in bipartite-local coordinates /// - /// # Panics + /// # Errors /// - /// Panics if any edge references an out-of-bounds left or right vertex index. - pub fn new(left_size: usize, right_size: usize, edges: Vec<(usize, usize)>) -> Self { + /// Returns an error if any edge references an out-of-bounds left or right vertex index. + pub fn new( + left_size: usize, + right_size: usize, + edges: Vec<(usize, usize)>, + ) -> Result { + left_size + .checked_add(right_size) + .ok_or("bipartite vertex count overflows usize")?; for &(u, v) in &edges { - assert!( - u < left_size, - "left vertex {} out of bounds (left_size={})", - u, - left_size - ); - assert!( - v < right_size, - "right vertex {} out of bounds (right_size={})", - v, - right_size - ); + if !(u < left_size) { + return Err( + format!("left vertex {} out of bounds (left_size={})", u, left_size).into(), + ); + } + if !(v < right_size) { + return Err(format!( + "right vertex {} out of bounds (right_size={})", + v, right_size + ) + .into()); + } } - Self { + Ok(Self { left_size, right_size, edges, - } + }) } /// Returns the number of vertices in the left partition. diff --git a/src/topology/directed_graph.rs b/src/topology/directed_graph.rs index 84fe30027..d01b9ef9b 100644 --- a/src/topology/directed_graph.rs +++ b/src/topology/directed_graph.rs @@ -28,12 +28,12 @@ use serde::{Deserialize, Serialize}; /// ``` /// use problemreductions::topology::DirectedGraph; /// -/// let g = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); +/// let g = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); /// assert_eq!(g.num_vertices(), 3); /// assert_eq!(g.num_arcs(), 2); /// assert!(g.is_dag()); /// -/// let cyclic = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); +/// let cyclic = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); /// assert!(!cyclic.is_dag()); /// ``` #[derive(Debug, Clone)] @@ -49,30 +49,37 @@ impl DirectedGraph { /// * `num_vertices` - Number of vertices in the graph /// * `arcs` - List of arcs as `(source, target)` pairs /// - /// # Panics + /// # Errors /// - /// Panics if any arc references a vertex index >= `num_vertices`. - pub fn new(num_vertices: usize, arcs: Vec<(usize, usize)>) -> Self { + /// Returns an error if any arc references a vertex index >= `num_vertices`. + pub fn new( + num_vertices: usize, + arcs: Vec<(usize, usize)>, + ) -> Result { let mut inner = DiGraph::new(); for _ in 0..num_vertices { inner.add_node(()); } for (u, v) in arcs { - assert!( - u < num_vertices && v < num_vertices, - "arc ({}, {}) references vertex >= num_vertices ({})", - u, - v, - num_vertices - ); + if !(u < num_vertices && v < num_vertices) { + return Err(format!( + "arc ({}, {}) references vertex >= num_vertices ({})", + u, v, num_vertices + ) + .into()); + } inner.add_edge(NodeIndex::new(u), NodeIndex::new(v), ()); } - Self { inner } + Ok(Self { inner }) } /// Creates an empty directed graph with the given number of vertices and no arcs. pub fn empty(num_vertices: usize) -> Self { - Self::new(num_vertices, vec![]) + let mut inner = DiGraph::new(); + for _ in 0..num_vertices { + inner.add_node(()); + } + Self { inner } } /// Returns the number of vertices in the graph. @@ -222,7 +229,7 @@ impl DirectedGraph { .map(|(u, v)| (new_index[u], new_index[v])) .collect(); - Self::new(count, new_arcs) + Self::new(count, new_arcs).expect("generated graph endpoints are in range") } } @@ -263,7 +270,7 @@ impl<'de> Deserialize<'de> for DirectedGraph { arcs: Vec<(usize, usize)>, } let data = GraphData::deserialize(deserializer)?; - Ok(DirectedGraph::new(data.num_vertices, data.arcs)) + DirectedGraph::new(data.num_vertices, data.arcs).map_err(serde::de::Error::custom) } } diff --git a/src/topology/graph.rs b/src/topology/graph.rs index 263b64aeb..b2a457d48 100644 --- a/src/topology/graph.rs +++ b/src/topology/graph.rs @@ -93,7 +93,7 @@ pub trait Graph: Clone + Send + Sync + 'static { /// use problemreductions::topology::SimpleGraph; /// use problemreductions::topology::Graph; /// -/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); +/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); /// assert_eq!(graph.num_vertices(), 4); /// assert_eq!(graph.num_edges(), 3); /// assert!(graph.has_edge(0, 1)); @@ -112,30 +112,37 @@ impl SimpleGraph { /// * `num_vertices` - Number of vertices in the graph /// * `edges` - List of edges as (u, v) pairs /// - /// # Panics + /// # Errors /// - /// Panics if any edge references a vertex index >= num_vertices. - pub fn new(num_vertices: usize, edges: Vec<(usize, usize)>) -> Self { + /// Returns an error if any edge references a vertex index >= num_vertices. + pub fn new( + num_vertices: usize, + edges: Vec<(usize, usize)>, + ) -> Result { let mut inner = UnGraph::new_undirected(); for _ in 0..num_vertices { inner.add_node(()); } for (u, v) in edges { - assert!( - u < num_vertices && v < num_vertices, - "edge ({}, {}) references vertex >= num_vertices ({})", - u, - v, - num_vertices - ); + if !(u < num_vertices && v < num_vertices) { + return Err(format!( + "edge ({}, {}) references vertex >= num_vertices ({})", + u, v, num_vertices + ) + .into()); + } inner.add_edge(NodeIndex::new(u), NodeIndex::new(v), ()); } - Self { inner } + Ok(Self { inner }) } /// Creates an empty graph with the given number of vertices. pub fn empty(num_vertices: usize) -> Self { - Self::new(num_vertices, vec![]) + let mut inner = UnGraph::new_undirected(); + for _ in 0..num_vertices { + inner.add_node(()); + } + Self { inner } } /// Creates a complete graph (all vertices connected). @@ -146,7 +153,7 @@ impl SimpleGraph { edges.push((i, j)); } } - Self::new(num_vertices, edges) + Self::new(num_vertices, edges).expect("generated graph endpoints are in range") } /// Creates a path graph (0-1-2-...-n). @@ -154,7 +161,7 @@ impl SimpleGraph { let edges: Vec<_> = (0..num_vertices.saturating_sub(1)) .map(|i| (i, i + 1)) .collect(); - Self::new(num_vertices, edges) + Self::new(num_vertices, edges).expect("generated graph endpoints are in range") } /// Creates a cycle graph (0-1-2-...-n-0). @@ -164,20 +171,22 @@ impl SimpleGraph { } let mut edges: Vec<_> = (0..num_vertices - 1).map(|i| (i, i + 1)).collect(); edges.push((num_vertices - 1, 0)); - Self::new(num_vertices, edges) + Self::new(num_vertices, edges).expect("generated graph endpoints are in range") } /// Creates a star graph (vertex 0 connected to all others). pub fn star(num_vertices: usize) -> Self { let edges: Vec<_> = (1..num_vertices).map(|i| (0, i)).collect(); - Self::new(num_vertices, edges) + Self::new(num_vertices, edges).expect("generated graph endpoints are in range") } /// Creates a grid graph with the given dimensions. /// /// Vertices are numbered row by row: vertex `r * cols + c` is at row `r`, column `c`. - pub fn grid(rows: usize, cols: usize) -> Self { - let num_vertices = rows * cols; + pub fn grid(rows: usize, cols: usize) -> Result { + let num_vertices = rows + .checked_mul(cols) + .ok_or("grid vertex count overflows usize")?; let mut edges = Vec::new(); for r in 0..rows { @@ -279,7 +288,7 @@ impl<'de> Deserialize<'de> for SimpleGraph { edges: Vec<(usize, usize)>, } let data = GraphData::deserialize(deserializer)?; - Ok(SimpleGraph::new(data.num_vertices, data.edges)) + SimpleGraph::new(data.num_vertices, data.edges).map_err(serde::de::Error::custom) } } diff --git a/src/topology/mixed_graph.rs b/src/topology/mixed_graph.rs index 9d95fbac9..96a7b0eab 100644 --- a/src/topology/mixed_graph.rs +++ b/src/topology/mixed_graph.rs @@ -12,49 +12,72 @@ use serde::{Deserialize, Serialize}; /// so higher-level models can use that order as part of their configuration /// semantics, but edge-membership queries treat them as unordered pairs. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MixedGraphData")] pub struct MixedGraph { num_vertices: usize, arcs: Vec<(usize, usize)>, edges: Vec<(usize, usize)>, } +#[derive(Deserialize)] +struct MixedGraphData { + num_vertices: usize, + arcs: Vec<(usize, usize)>, + edges: Vec<(usize, usize)>, +} + +impl TryFrom for MixedGraph { + type Error = crate::registry::ConstructionError; + fn try_from(data: MixedGraphData) -> Result { + Self::new(data.num_vertices, data.arcs, data.edges) + } +} + impl MixedGraph { /// Create a new mixed graph. /// - /// # Panics + /// # Errors /// - /// Panics if any endpoint references a vertex outside `0..num_vertices`. - pub fn new(num_vertices: usize, arcs: Vec<(usize, usize)>, edges: Vec<(usize, usize)>) -> Self { + /// Returns an error if any endpoint references a vertex outside `0..num_vertices`. + pub fn new( + num_vertices: usize, + arcs: Vec<(usize, usize)>, + edges: Vec<(usize, usize)>, + ) -> Result { for &(u, v) in &arcs { - assert!( - u < num_vertices && v < num_vertices, - "arc ({}, {}) references vertex >= num_vertices ({})", - u, - v, - num_vertices - ); + if !(u < num_vertices && v < num_vertices) { + return Err(format!( + "arc ({}, {}) references vertex >= num_vertices ({})", + u, v, num_vertices + ) + .into()); + } } for &(u, v) in &edges { - assert!( - u < num_vertices && v < num_vertices, - "edge ({}, {}) references vertex >= num_vertices ({})", - u, - v, - num_vertices - ); + if !(u < num_vertices && v < num_vertices) { + return Err(format!( + "edge ({}, {}) references vertex >= num_vertices ({})", + u, v, num_vertices + ) + .into()); + } } - Self { + Ok(Self { num_vertices, arcs, edges, - } + }) } /// Create an empty mixed graph with no arcs or undirected edges. pub fn empty(num_vertices: usize) -> Self { - Self::new(num_vertices, vec![], vec![]) + Self { + num_vertices, + arcs: vec![], + edges: vec![], + } } /// Return the number of vertices. diff --git a/src/topology/planar_graph.rs b/src/topology/planar_graph.rs index a29a2b042..bbd7a973c 100644 --- a/src/topology/planar_graph.rs +++ b/src/topology/planar_graph.rs @@ -15,11 +15,11 @@ use serde::{Deserialize, Serialize}; /// /// // K4 is planar: 4 vertices, 6 edges, 6 <= 3*4 - 6 = 6 /// let edges = vec![(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)]; -/// let g = PlanarGraph::new(4, edges); +/// let g = PlanarGraph::new(4, edges).unwrap(); /// assert_eq!(g.num_vertices(), 4); /// assert_eq!(g.num_edges(), 6); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct PlanarGraph { inner: SimpleGraph, } @@ -27,21 +27,13 @@ pub struct PlanarGraph { impl PlanarGraph { /// Create a new planar graph. /// - /// # Panics - /// Panics if the graph violates the necessary planarity condition |E| <= 3|V| - 6. - pub fn new(num_vertices: usize, edges: Vec<(usize, usize)>) -> Self { - let inner = SimpleGraph::new(num_vertices, edges); - if num_vertices >= 3 { - let max_edges = 3 * num_vertices - 6; - assert!( - inner.num_edges() <= max_edges, - "graph has {} edges but a planar graph on {} vertices can have at most {} edges", - inner.num_edges(), - num_vertices, - max_edges - ); - } - Self { inner } + /// # Errors + /// Returns an error if the graph violates the necessary planarity condition |E| <= 3|V| - 6. + pub fn new( + num_vertices: usize, + edges: Vec<(usize, usize)>, + ) -> Result { + Self::try_from(SimpleGraph::new(num_vertices, edges)?) } /// Get a reference to the underlying SimpleGraph. @@ -50,6 +42,31 @@ impl PlanarGraph { } } +impl TryFrom for PlanarGraph { + type Error = crate::registry::ConstructionError; + fn try_from(inner: SimpleGraph) -> Result { + let num_vertices = inner.num_vertices(); + if num_vertices >= 3 { + let max_edges = 3 * (num_vertices as u128) - 6; + if inner.num_edges() as u128 > max_edges { + return Err(format!("graph has {} edges but a planar graph on {num_vertices} vertices can have at most {max_edges} edges", inner.num_edges()).into()); + } + } + Ok(Self { inner }) + } +} + +impl<'de> Deserialize<'de> for PlanarGraph { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + struct PlanarGraphData { + inner: SimpleGraph, + } + let data = PlanarGraphData::deserialize(deserializer)?; + Self::try_from(data.inner).map_err(serde::de::Error::custom) + } +} + impl Graph for PlanarGraph { const NAME: &'static str = "PlanarGraph"; diff --git a/src/truth_table.rs b/src/truth_table.rs index 479b02983..a829ba264 100644 --- a/src/truth_table.rs +++ b/src/truth_table.rs @@ -3,6 +3,7 @@ //! This module provides a `TruthTable` type for representing boolean functions //! and their truth tables, useful for constructing logic gadgets in reductions. +use crate::registry::ConstructionError; use bitvec::prelude::*; use serde::{Deserialize, Serialize}; @@ -45,10 +46,8 @@ impl<'de> Deserialize<'de> for TruthTable { D: serde::Deserializer<'de>, { let serde_repr = TruthTableSerde::deserialize(deserializer)?; - Ok(TruthTable { - num_inputs: serde_repr.num_inputs, - outputs: serde_repr.outputs.into_iter().collect(), - }) + Self::from_outputs(serde_repr.num_inputs, serde_repr.outputs) + .map_err(serde::de::Error::custom) } } @@ -57,42 +56,64 @@ impl TruthTable { /// /// The outputs vector must have exactly 2^num_inputs elements. /// Index i corresponds to the input where the j-th bit represents variable j. - pub fn from_outputs(num_inputs: usize, outputs: Vec) -> Self { - let expected_len = 1 << num_inputs; - assert_eq!( - outputs.len(), - expected_len, - "outputs length must be 2^num_inputs = {}, got {}", - expected_len, - outputs.len() - ); - - let bits: BitVec = outputs.into_iter().collect(); - Self { + pub fn from_outputs(num_inputs: usize, outputs: Vec) -> Result { + let expected_len = Self::row_count(num_inputs)?; + if outputs.len() != expected_len { + return Err(ConstructionError::InvalidInput(format!( + "outputs length must be 2^num_inputs = {expected_len}, got {}", + outputs.len() + ))); + } + let mut bits = Self::allocate_outputs(expected_len)?; + for (mut bit, output) in bits.iter_mut().zip(outputs) { + *bit = output; + } + Ok(Self { num_inputs, outputs: bits, - } + }) } - /// Create a truth table from a function. - /// - /// The function takes a slice of booleans (the input) and returns the output. - pub fn from_function(num_inputs: usize, f: F) -> Self + /// Create a truth table by evaluating a function for each input combination. + pub fn from_function(num_inputs: usize, f: F) -> Result where F: Fn(&[bool]) -> bool, { - let num_rows = 1 << num_inputs; - let mut outputs = BitVec::with_capacity(num_rows); - + let num_rows = Self::row_count(num_inputs)?; + let mut outputs = Self::allocate_outputs(num_rows)?; + let mut input = vec![false; num_inputs]; for i in 0..num_rows { - let input: Vec = (0..num_inputs).map(|j| (i >> j) & 1 == 1).collect(); - outputs.push(f(&input)); + for (j, bit) in input.iter_mut().enumerate() { + *bit = (i >> j) & 1 == 1; + } + outputs.set(i, f(&input)); } - - Self { + Ok(Self { num_inputs, outputs, - } + }) + } + + fn row_count(num_inputs: usize) -> Result { + u32::try_from(num_inputs) + .ok() + .and_then(|shift| 1usize.checked_shl(shift)) + .filter(|&rows| rows <= BitSlice::::MAX_BITS) + .ok_or_else(|| { + ConstructionError::IntegerOverflow("representing truth-table rows".into()) + }) + } + + fn allocate_outputs(num_rows: usize) -> Result { + let words = num_rows.div_ceil(usize::BITS as usize); + let mut storage = Vec::::new(); + storage.try_reserve_exact(words).map_err(|error| { + ConstructionError::Conversion(format!("allocating truth-table storage: {error}")) + })?; + storage.resize(words, 0); + let mut outputs = BitVec::from_vec(storage); + outputs.truncate(num_rows); + Ok(outputs) } /// Get the number of input variables. @@ -102,7 +123,7 @@ impl TruthTable { /// Get the number of rows (2^num_inputs). pub fn num_rows(&self) -> usize { - 1 << self.num_inputs + self.outputs.len() } /// Evaluate the truth table for a given input. @@ -183,39 +204,39 @@ impl TruthTable { } /// Create an AND gate truth table. - pub fn and(num_inputs: usize) -> Self { + pub fn and(num_inputs: usize) -> Result { Self::from_function(num_inputs, |input| input.iter().all(|&b| b)) } /// Create an OR gate truth table. - pub fn or(num_inputs: usize) -> Self { + pub fn or(num_inputs: usize) -> Result { Self::from_function(num_inputs, |input| input.iter().any(|&b| b)) } /// Create a NOT gate truth table (1 input). pub fn not() -> Self { - Self::from_outputs(1, vec![true, false]) + Self::from_outputs(1, vec![true, false]).expect("NOT has two rows") } /// Create an XOR gate truth table. - pub fn xor(num_inputs: usize) -> Self { + pub fn xor(num_inputs: usize) -> Result { Self::from_function(num_inputs, |input| { input.iter().filter(|&&b| b).count() % 2 == 1 }) } /// Create a NAND gate truth table. - pub fn nand(num_inputs: usize) -> Self { + pub fn nand(num_inputs: usize) -> Result { Self::from_function(num_inputs, |input| !input.iter().all(|&b| b)) } /// Create a NOR gate truth table. - pub fn nor(num_inputs: usize) -> Self { + pub fn nor(num_inputs: usize) -> Result { Self::from_function(num_inputs, |input| !input.iter().any(|&b| b)) } /// Create an XNOR gate truth table. - pub fn xnor(num_inputs: usize) -> Self { + pub fn xnor(num_inputs: usize) -> Result { Self::from_function(num_inputs, |input| { input.iter().filter(|&&b| b).count().is_multiple_of(2) }) @@ -225,7 +246,7 @@ impl TruthTable { /// Input 0 is 'a', input 1 is 'b'. pub fn implies() -> Self { // Index 0: [F,F] -> T, Index 1: [T,F] -> F, Index 2: [F,T] -> T, Index 3: [T,T] -> T - Self::from_outputs(2, vec![true, false, true, true]) + Self::from_outputs(2, vec![true, false, true, true]).expect("implication has four rows") } /// Combine two truth tables using AND. diff --git a/src/types.rs b/src/types.rs index 03146feba..9de4d611d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,13 +4,13 @@ use serde::de::{self, DeserializeOwned, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt; -/// Largest integer magnitude represented exactly by an IEEE 754 `f64`. +/// Maximum integer magnitude accepted by the exact `i64` to `f64` conversion. pub const MAX_EXACT_F64_INTEGER: i64 = (1_i64 << 53) - 1; -/// An `i64` cannot cross an exact-integer `f64` boundary without precision loss. +/// An `i64` is outside the supported exact-integer `f64` conversion range. #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] #[error( - "integer {value} is outside the exactly representable f64 range [{min}, {max}]", + "integer {value} is outside the supported exact-integer f64 conversion range [{min}, {max}]", min = -MAX_EXACT_F64_INTEGER, max = MAX_EXACT_F64_INTEGER )] @@ -29,7 +29,8 @@ pub enum NumericArithmeticError { NonFiniteResult, } -/// Convert an `i64` to `f64` only when the integer value remains exact. +/// Convert an `i64` to `f64` within the supported range ±(2^53 − 1). +/// Values outside this range are rejected even if individually representable. pub fn i64_to_exact_f64(value: i64) -> Result { if (-MAX_EXACT_F64_INTEGER..=MAX_EXACT_F64_INTEGER).contains(&value) { Ok(value as f64) @@ -38,7 +39,8 @@ pub fn i64_to_exact_f64(value: i64) -> Result { } } -/// Bound for objective value types (i64, f64, etc.) +/// Bound for objective value types (i64, f64, etc.). +/// Integers reject overflow; floats allow rounding and reject non-finite results. pub trait NumericSize: Clone + Default @@ -49,9 +51,9 @@ pub trait NumericSize: + std::ops::AddAssign + 'static { - /// Add two values when the exact result remains representable and finite. + /// Checked addition. fn checked_add_value(self, other: Self) -> Result; - /// Multiply two values when the exact result remains representable and finite. + /// Checked multiplication. fn checked_mul_value(self, other: Self) -> Result; } @@ -304,12 +306,6 @@ pub trait Aggregate: Clone + fmt::Debug + Serialize + DeserializeOwned { } } -/// Aggregate value whose optimum identifies contributing solutions. -pub trait SolutionAggregate: Aggregate { - /// Whether a solution-level value contributes to the final aggregate value. - fn contributes_to_solution(value: &Self, total: &Self) -> bool; -} - /// Maximum aggregate over feasible values. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Max(pub Option); @@ -338,14 +334,6 @@ impl Aggregat } } -impl SolutionAggregate - for Max -{ - fn contributes_to_solution(value: &Self, total: &Self) -> bool { - matches!((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 { @@ -397,14 +385,6 @@ impl Aggregat } } -impl SolutionAggregate - for Min -{ - fn contributes_to_solution(value: &Self, total: &Self) -> bool { - matches!((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 { @@ -508,12 +488,6 @@ impl Aggregate for Or { } } -impl SolutionAggregate for Or { - fn contributes_to_solution(value: &Self, total: &Self) -> bool { - value.0 && total.0 - } -} - impl fmt::Display for Or { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Or({})", self.0) @@ -648,17 +622,6 @@ impl Aggregat } } -impl SolutionAggregate - for Extremum -{ - fn contributes_to_solution(candidate: &Self, total: &Self) -> bool { - matches!( - (candidate.value.as_ref(), total.value.as_ref()), - (Some(value), Some(best)) if candidate.sense == total.sense && value == best - ) - } -} - impl fmt::Display for Extremum { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match (&self.sense, &self.value) { diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index 73d85007f..37f0a3f81 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -712,29 +712,15 @@ fn rule_specs_solution_pairs_are_consistent() { .unwrap_or_else(|error| { panic!("Rule {label}: source configuration evaluation failed: {error}") }); - assert_ne!( - source_eval, "Max(None)", - "Rule {label}: source_config evaluates to Max(None)" + assert!( + source_eval.1, + "Rule {label}: infeasible source configuration: {}", + source_eval.0 ); - assert_ne!( - source_eval, "Min(None)", - "Rule {label}: source_config evaluates to Min(None)" - ); - assert_ne!( - source_eval, "Or(false)", - "Rule {label}: source_config evaluates to Or(false)" - ); - assert_ne!( - 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)" + assert!( + target_eval.1, + "Rule {label}: infeasible target configuration: {}", + target_eval.0 ); // Round-trip: extract_solution(target_config) must produce a valid // source config with the same evaluation value (witness paths only) diff --git a/src/unit_tests/export.rs b/src/unit_tests/export.rs index 37b14075a..9e242e986 100644 --- a/src/unit_tests/export.rs +++ b/src/unit_tests/export.rs @@ -214,8 +214,8 @@ fn problem_side_from_typed_problem() { use crate::models::graph::MaximumIndependentSet; use crate::topology::SimpleGraph; - let g = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let mis = MaximumIndependentSet::new(g, vec![1, 1, 1]); + let g = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let mis = MaximumIndependentSet::new(g, vec![1, 1, 1]).unwrap(); let side = ProblemSide::from_problem(&mis); assert_eq!(side.problem, "MaximumIndependentSet"); assert_eq!(side.variant["graph"], "SimpleGraph"); diff --git a/src/unit_tests/graph_models.rs b/src/unit_tests/graph_models.rs index 675a9c487..4721a5144 100644 --- a/src/unit_tests/graph_models.rs +++ b/src/unit_tests/graph_models.rs @@ -24,17 +24,20 @@ mod maximum_independent_set { #[test] fn test_creation() { let problem = MaximumIndependentSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.num_variables(), 4); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] fn test_with_weights() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1, 2, 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1, 2, 3]) + .unwrap(); assert_eq!(problem.weights().to_vec(), vec![1, 2, 3]); assert!(problem.is_weighted()); } @@ -42,14 +45,19 @@ mod maximum_independent_set { #[test] fn test_unweighted() { // i64 type is always considered weighted, even with uniform values - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); assert!(problem.is_weighted()); } #[test] fn test_has_edge() { - let problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); assert!(problem.graph().has_edge(0, 1)); assert!(problem.graph().has_edge(1, 0)); // Undirected assert!(problem.graph().has_edge(1, 2)); @@ -58,8 +66,11 @@ mod maximum_independent_set { #[test] fn test_evaluate_valid() { - let problem = - MaximumIndependentSet::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), vec![1i64; 4]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(), + vec![1i64; 4], + ) + .unwrap(); // Valid: select 0 and 2 (not adjacent) assert_eq!( @@ -76,8 +87,11 @@ mod maximum_independent_set { #[test] fn test_evaluate_invalid() { - let problem = - MaximumIndependentSet::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), vec![1i64; 4]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(), + vec![1i64; 4], + ) + .unwrap(); // Invalid: 0 and 1 are adjacent - returns Invalid assert_eq!( @@ -94,8 +108,11 @@ mod maximum_independent_set { #[test] fn test_evaluate_empty() { - let problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); // Empty selection is valid with size 0 assert_eq!( problem.evaluate(&vec![false, false, false]).unwrap(), @@ -105,8 +122,11 @@ mod maximum_independent_set { #[test] fn test_evaluate_weighted() { - let problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![10, 20, 30]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(3, vec![(0, 1)]).unwrap(), + vec![10, 20, 30], + ) + .unwrap(); // Select vertex 2 (weight 30) assert_eq!( @@ -125,9 +145,10 @@ mod maximum_independent_set { fn test_brute_force_triangle() { // Triangle graph: maximum IS has size 1 let problem = MaximumIndependentSet::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -142,9 +163,10 @@ mod maximum_independent_set { fn test_brute_force_path() { // Path graph 0-1-2-3: maximum IS = {0,2} or {1,3} or {0,3} let problem = MaximumIndependentSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -160,8 +182,11 @@ mod maximum_independent_set { #[test] fn test_brute_force_weighted() { // Graph with weights: vertex 1 has high weight but is connected to both 0 and 2 - let problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 100, 1]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1, 100, 1], + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -173,36 +198,41 @@ mod maximum_independent_set { #[test] fn test_is_independent_set_function() { assert!(is_independent_set( - &SimpleGraph::new(3, vec![(0, 1)]), + &SimpleGraph::new(3, vec![(0, 1)]).unwrap(), &[true, false, true] )); assert!(is_independent_set( - &SimpleGraph::new(3, vec![(0, 1)]), + &SimpleGraph::new(3, vec![(0, 1)]).unwrap(), &[false, true, true] )); assert!(!is_independent_set( - &SimpleGraph::new(3, vec![(0, 1)]), + &SimpleGraph::new(3, vec![(0, 1)]).unwrap(), &[true, true, false] )); assert!(is_independent_set( - &SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), &[true, false, true] )); assert!(!is_independent_set( - &SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), &[false, true, true] )); } #[test] fn test_direction() { - let _problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let _problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); } #[test] fn test_edges() { - let problem = - MaximumIndependentSet::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), vec![1i64; 4]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(), + vec![1i64; 4], + ) + .unwrap(); let edges = problem.graph().edges(); assert_eq!(edges.len(), 2); assert!(edges.contains(&(0, 1)) || edges.contains(&(1, 0))); @@ -212,13 +242,16 @@ mod maximum_independent_set { #[test] fn test_with_custom_weights() { let problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![5, 10, 15]); + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![5, 10, 15]) + .unwrap(); assert_eq!(problem.weights().to_vec(), vec![5, 10, 15]); } #[test] fn test_empty_graph() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![]), vec![1i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![]).unwrap(), vec![1i64; 3]) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -229,8 +262,11 @@ mod maximum_independent_set { #[test] fn test_validity_via_evaluate() { - let problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); // Valid IS configurations return is_valid() == true assert!(problem @@ -263,25 +299,31 @@ mod minimum_vertex_cover { #[test] fn test_creation() { let problem = MinimumVertexCover::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.num_variables(), 4); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] fn test_with_weights() { - let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1, 2, 3]); + let problem = + MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1, 2, 3]) + .unwrap(); assert_eq!(problem.weights().to_vec(), vec![1, 2, 3]); assert!(problem.is_weighted()); } #[test] fn test_evaluate_valid() { - let problem = - MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); // Valid: select vertex 1 (covers both edges) assert_eq!( @@ -298,8 +340,11 @@ mod minimum_vertex_cover { #[test] fn test_evaluate_invalid() { - let problem = - MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); // Invalid: no vertex selected - returns Invalid for minimization assert_eq!( @@ -317,8 +362,11 @@ mod minimum_vertex_cover { #[test] fn test_brute_force_path() { // Path graph 0-1-2: minimum vertex cover is {1} - let problem = - MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -330,9 +378,10 @@ mod minimum_vertex_cover { fn test_brute_force_triangle() { // Triangle: minimum vertex cover has size 2 let problem = MinimumVertexCover::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -348,8 +397,11 @@ mod minimum_vertex_cover { #[test] fn test_brute_force_weighted() { // Weighted: prefer selecting low-weight vertices - let problem = - MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![100, 1, 100]); + let problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![100, 1, 100], + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -361,31 +413,34 @@ mod minimum_vertex_cover { #[test] fn test_is_vertex_cover_function() { assert!(is_vertex_cover( - &SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), &[false, true, false] )); assert!(is_vertex_cover( - &SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), &[true, false, true] )); assert!(!is_vertex_cover( - &SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), &[true, false, false] )); assert!(!is_vertex_cover( - &SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), &[false, false, false] )); } #[test] fn test_direction() { - let _problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let _problem = + MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); } #[test] fn test_empty_graph() { - let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![]), vec![1i64; 3]); + let problem = + MinimumVertexCover::new(SimpleGraph::new(3, vec![]).unwrap(), vec![1i64; 3]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -396,7 +451,9 @@ mod minimum_vertex_cover { #[test] fn test_single_edge() { - let problem = MinimumVertexCover::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i64; 2]); + let problem = + MinimumVertexCover::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![1i64; 2]) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -406,8 +463,11 @@ mod minimum_vertex_cover { #[test] fn test_validity_via_evaluate() { - let problem = - MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); // Valid cover configurations return is_valid() == true assert!(problem @@ -434,8 +494,10 @@ mod minimum_vertex_cover { // For a graph, if S is an independent set, then V\S is a vertex cover let edges = vec![(0, 1), (1, 2), (2, 3)]; let is_problem = - MaximumIndependentSet::new(SimpleGraph::new(4, edges.clone()), vec![1i64; 4]); - let vc_problem = MinimumVertexCover::new(SimpleGraph::new(4, edges), vec![1i64; 4]); + MaximumIndependentSet::new(SimpleGraph::new(4, edges.clone()).unwrap(), vec![1i64; 4]) + .unwrap(); + let vc_problem = + MinimumVertexCover::new(SimpleGraph::new(4, edges).unwrap(), vec![1i64; 4]).unwrap(); let solver = BruteForce::new(); @@ -450,7 +512,9 @@ mod minimum_vertex_cover { #[test] fn test_with_custom_weights() { - let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1, 2, 3]); + let problem = + MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1, 2, 3]) + .unwrap(); assert!(problem.is_weighted()); assert_eq!(problem.weights().to_vec(), vec![1, 2, 3]); } @@ -458,7 +522,8 @@ mod minimum_vertex_cover { #[test] fn test_is_weighted_empty() { // i64 type is always considered weighted, even with empty weights - let problem = MinimumVertexCover::new(SimpleGraph::new(0, vec![]), vec![0i64; 0]); + let problem = + MinimumVertexCover::new(SimpleGraph::new(0, vec![]).unwrap(), vec![0i64; 0]).unwrap(); assert!(problem.is_weighted()); } @@ -466,7 +531,7 @@ mod minimum_vertex_cover { #[should_panic(expected = "selected length must match num_vertices")] fn test_is_vertex_cover_wrong_len() { // Wrong length should panic - is_vertex_cover(&SimpleGraph::new(3, vec![(0, 1)]), &[true, false]); + is_vertex_cover(&SimpleGraph::new(3, vec![(0, 1)]).unwrap(), &[true, false]); } } @@ -493,16 +558,21 @@ mod integral_flow_homologous_arcs { (3, 5), (4, 5), ], - ), + ) + .unwrap(), vec![1; 8], 0, 5, 2, vec![(2, 5), (4, 3)], - ); + ) + .unwrap(); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_arcs(), 8); - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); } } @@ -515,16 +585,17 @@ mod kcoloring { #[test] fn test_creation() { - let problem = KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = + KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); assert_eq!(problem.num_colors(), 3); - assert_eq!(problem.num_variables(), 4); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] fn test_evaluate_valid() { - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); // Valid: different colors on adjacent vertices - returns true assert!(problem.evaluate(&vec![0, 1, 0]).unwrap()); @@ -533,7 +604,7 @@ mod kcoloring { #[test] fn test_evaluate_invalid() { - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); // Invalid: adjacent vertices have same color assert!(!problem.evaluate(&vec![0, 0, 1]).unwrap()); // 0-1 conflict @@ -543,7 +614,8 @@ mod kcoloring { #[test] fn test_brute_force_path() { // Path graph can be 2-colored - let problem = KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = + KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -556,7 +628,8 @@ mod kcoloring { #[test] fn test_brute_force_triangle() { // Triangle needs 3 colors - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let problem = + KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -572,7 +645,8 @@ mod kcoloring { #[test] fn test_triangle_2_colors_unsat() { // Triangle cannot be 2-colored - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let problem = + KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); let solver = BruteForce::new(); // No satisfying assignments @@ -582,7 +656,7 @@ mod kcoloring { #[test] fn test_is_valid_coloring_function() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); assert!(is_valid_coloring(&graph, &[0, 1, 0], 2)); assert!(is_valid_coloring(&graph, &[0, 1, 2], 3)); @@ -594,13 +668,13 @@ mod kcoloring { #[test] #[should_panic(expected = "coloring length must match num_vertices")] fn test_is_valid_coloring_wrong_len() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); is_valid_coloring(&graph, &[0, 1], 2); // Wrong length } #[test] fn test_empty_graph() { - let problem = KColoring::::new(SimpleGraph::new(3, vec![])); + let problem = KColoring::::new(SimpleGraph::new(3, vec![]).unwrap()); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -611,10 +685,9 @@ mod kcoloring { #[test] fn test_complete_graph_k4() { // K4 needs 4 colors - let problem = KColoring::::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )); + let problem = KColoring::::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), + ); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); diff --git a/src/unit_tests/io.rs b/src/unit_tests/io.rs index e3e9e148b..f6db51274 100644 --- a/src/unit_tests/io.rs +++ b/src/unit_tests/io.rs @@ -6,8 +6,11 @@ use std::time::{SystemTime, UNIX_EPOCH}; #[test] fn test_to_json() { - let problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); let json = to_json(&problem); assert!(json.is_ok()); let json = json.unwrap(); @@ -16,8 +19,11 @@ fn test_to_json() { #[test] fn test_from_json() { - let problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); let json = to_json(&problem).unwrap(); let restored: MaximumIndependentSet = from_json(&json).unwrap(); assert_eq!(restored.graph().num_vertices(), 3); @@ -26,7 +32,9 @@ fn test_from_json() { #[test] fn test_json_compact() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); let compact = to_json_compact(&problem).unwrap(); let pretty = to_json(&problem).unwrap(); // Compact should be shorter @@ -36,9 +44,10 @@ fn test_json_compact() { #[test] fn test_file_roundtrip() { let problem = MaximumIndependentSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let ts = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() diff --git a/src/unit_tests/models/algebraic/algebraic_equations_over_gf2.rs b/src/unit_tests/models/algebraic/algebraic_equations_over_gf2.rs index 87df06fc5..804ad6c59 100644 --- a/src/unit_tests/models/algebraic/algebraic_equations_over_gf2.rs +++ b/src/unit_tests/models/algebraic/algebraic_equations_over_gf2.rs @@ -1,6 +1,5 @@ use crate::models::algebraic::AlgebraicEquationsOverGF2; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Or; @@ -39,7 +38,10 @@ fn test_algebraic_equations_over_gf2_creation_and_accessors() { assert_eq!(p.num_variables(), 3); assert_eq!(p.num_equations(), 3); assert_eq!(p.equations().len(), 3); - assert_eq!(p.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] + ); assert_eq!(p.num_variables(), 3); assert_eq!( ::NAME, @@ -71,7 +73,10 @@ fn test_algebraic_equations_over_gf2_evaluate_satisfiable() { #[test] fn test_algebraic_equations_over_gf2_evaluate_unsatisfiable() { let p = unsatisfiable_problem(); - assert_eq!(p.dimensions(), vec![2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2] + ); // All 4 assignments should fail assert_eq!(p.evaluate(&vec![false, false]).unwrap(), Or(false)); // eq0: 0+0=0 ✓, eq1: 0+0+1=1 ✗ assert_eq!(p.evaluate(&vec![false, true]).unwrap(), Or(false)); // eq0: 0+1=1 ✗ diff --git a/src/unit_tests/models/algebraic/bmf.rs b/src/unit_tests/models/algebraic/bmf.rs index a32c2a23c..ed51aa2b1 100644 --- a/src/unit_tests/models/algebraic/bmf.rs +++ b/src/unit_tests/models/algebraic/bmf.rs @@ -7,17 +7,17 @@ use crate::types::Min; #[test] fn test_bmf_creation() { let matrix = vec![vec![true, false], vec![false, true]]; - let problem = BMF::new(matrix, 2); + let problem = BMF::new(matrix, 2).unwrap(); assert_eq!(problem.rows(), 2); assert_eq!(problem.cols(), 2); assert_eq!(problem.rank(), 2); - assert_eq!(problem.num_variables(), 8); // 2*2 + 2*2 + assert_eq!(problem.num_variables().unwrap(), 8); // 2*2 + 2*2 } #[test] fn test_extract_factors() { let matrix = vec![vec![true]]; - let problem = BMF::new(matrix, 1); + let problem = BMF::new(matrix, 1).unwrap(); // Config: [b00, c00] = [1, 1] let solution = (vec![vec![true]], vec![vec![true]]); let (b, c) = problem.extract_factors(&solution); @@ -29,7 +29,7 @@ fn test_extract_factors() { fn test_extract_factors_larger() { // 2x2 matrix with rank 1 let matrix = vec![vec![true, true], vec![true, true]]; - let problem = BMF::new(matrix, 1); + let problem = BMF::new(matrix, 1).unwrap(); // B: 2x1, C: 1x2 // Config: [b00, b10, c00, c01] = [1, 1, 1, 1] let solution = (vec![vec![true], vec![true]], vec![vec![true, true]]); @@ -62,7 +62,7 @@ fn test_boolean_product_rank2() { fn test_hamming_distance() { // Target: [[1,0], [0,1]] let matrix = vec![vec![true, false], vec![false, true]]; - let problem = BMF::new(matrix, 2); + let problem = BMF::new(matrix, 2).unwrap(); // B = [[1,0], [0,1]], C = [[1,0], [0,1]] -> exact match // Config: [1,0,0,1, 1,0,0,1] @@ -80,7 +80,7 @@ fn test_hamming_distance() { #[test] fn test_evaluate() { let matrix = vec![vec![true, false], vec![false, true]]; - let problem = BMF::new(matrix, 2); + let problem = BMF::new(matrix, 2).unwrap(); // Exact factorization -> Min(Some(total_factor_size)) = 4 (two 1s in B, two in C) let config = ( @@ -96,7 +96,7 @@ fn test_evaluate() { #[test] fn test_evaluate_rejects_invalid_configurations() { - let problem = BMF::new(vec![vec![true]], 1); + let problem = BMF::new(vec![vec![true]], 1).unwrap(); assert!(Problem::evaluate(&problem, &(vec![], vec![vec![true]])).is_err()); assert!(Problem::evaluate(&problem, &(vec![vec![true]], vec![])).is_err()); assert!(Problem::evaluate(&problem, &(vec![vec![true, false]], vec![vec![true]])).is_err()); @@ -107,7 +107,7 @@ fn test_brute_force_ones() { // All-ones 2x2 factors exactly at rank 1: optimal total_factor_size = 4 // (B = [[1],[1]] has two 1s, C = [[1,1]] has two 1s). let matrix = vec![vec![true, true], vec![true, true]]; - let problem = BMF::new(matrix, 1); + let problem = BMF::new(matrix, 1).unwrap(); let solver = BruteForce::new(); let witnesses = solver.find_all_witnesses(&problem).unwrap(); @@ -122,7 +122,7 @@ fn test_brute_force_ones() { fn test_brute_force_identity() { // Identity matrix factors exactly at rank 2. let matrix = vec![vec![true, false], vec![false, true]]; - let problem = BMF::new(matrix, 2); + let problem = BMF::new(matrix, 2).unwrap(); let solver = BruteForce::new(); let witnesses = solver.find_all_witnesses(&problem).unwrap(); @@ -136,7 +136,7 @@ fn test_brute_force_insufficient_rank() { // Rank-1 over the 2x2 identity admits no exact factorization, // so every config evaluates to Min(None). let matrix = vec![vec![true, false], vec![false, true]]; - let problem = BMF::new(matrix, 1); + let problem = BMF::new(matrix, 1).unwrap(); let solver = BruteForce::new(); let witness = solver.solve(&problem).unwrap(); @@ -167,8 +167,8 @@ fn test_matrix_hamming_distance_function() { #[test] fn test_empty_matrix() { let matrix: Vec> = vec![]; - let problem = BMF::new(matrix, 1); - assert_eq!(problem.num_variables(), 0); + let problem = BMF::new(matrix, 1).unwrap(); + assert_eq!(problem.num_variables().unwrap(), 0); // Empty matrix factors exactly with zero factor size. assert_eq!( Problem::evaluate(&problem, &(vec![], vec![vec![]])).unwrap(), @@ -178,8 +178,11 @@ fn test_empty_matrix() { #[test] fn test_rank_zero_exactness() { - let nonzero = BMF::new(vec![vec![true, false]], 0); - assert_eq!(nonzero.dimensions(), Vec::::new()); + let nonzero = BMF::new(vec![vec![true, false]], 0).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&nonzero).unwrap(), + Vec::::new() + ); let empty_factors = (vec![vec![]], vec![]); assert_eq!(nonzero.hamming_distance(&empty_factors).unwrap(), 1); assert!(!nonzero.is_exact(&empty_factors).unwrap()); @@ -188,7 +191,7 @@ fn test_rank_zero_exactness() { Min(None) ); - let zero = BMF::new(vec![vec![false, false]], 0); + let zero = BMF::new(vec![vec![false, false]], 0).unwrap(); assert_eq!(zero.hamming_distance(&empty_factors).unwrap(), 0); assert!(zero.is_exact(&empty_factors).unwrap()); assert_eq!( @@ -200,7 +203,7 @@ fn test_rank_zero_exactness() { #[test] fn test_is_exact() { let matrix = vec![vec![true]]; - let problem = BMF::new(matrix, 1); + let problem = BMF::new(matrix, 1).unwrap(); assert!(problem .is_exact(&(vec![vec![true]], vec![vec![true]])) .unwrap()); @@ -215,10 +218,13 @@ fn test_bmf_problem() { // 2x2 identity matrix with rank 2 let matrix = vec![vec![true, false], vec![false, true]]; - let problem = BMF::new(matrix, 2); + let problem = BMF::new(matrix, 2).unwrap(); // dims: B(2*2) + C(2*2) = 8 binary variables - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); // Exact factorization: B = I, C = I — total factor size = 4 assert_eq!( @@ -245,8 +251,11 @@ fn test_bmf_problem() { // 1x1 matrix let matrix = vec![vec![true]]; - let problem = BMF::new(matrix, 1); - assert_eq!(problem.dimensions(), vec![2; 2]); // B(1*1) + C(1*1) + let problem = BMF::new(matrix, 1).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 2] + ); // B(1*1) + C(1*1) assert_eq!( Problem::evaluate(&problem, &(vec![vec![true]], vec![vec![true]])).unwrap(), Min(Some(2)) @@ -262,7 +271,8 @@ fn test_parameter_getters() { let problem = BMF::new( vec![vec![true, false], vec![false, true], vec![true, true]], 1, - ); + ) + .unwrap(); assert_eq!(problem.m(), 3); // rows assert_eq!(problem.n(), 2); // cols } @@ -275,7 +285,7 @@ fn test_bmf_paper_example() { vec![true, true, true], vec![false, true, true], ]; - let problem = BMF::new(matrix, 2); + let problem = BMF::new(matrix, 2).unwrap(); // B (3x2): [[1,0],[1,1],[0,1]], C (2x3): [[1,1,0],[0,1,1]] // Config: B row-major then C row-major // Eight 1s total -> optimal total factor size = 8. @@ -290,3 +300,19 @@ fn test_bmf_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert!(problem.is_exact(&best).unwrap()); } + +#[test] +fn json_rejects_invalid_instance() { + assert!( + serde_json::from_value::(serde_json::json!({"matrix":[[true],[]],"k":1})).is_err() + ); +} + +#[test] +fn deserialize_rebuilds_matrix_dimensions() { + let model: BMF = serde_json::from_value(serde_json::json!({ + "matrix": [[true, false]], "k": 1, "m": 99, "n": 99 + })) + .unwrap(); + assert_eq!((model.rows(), model.cols(), model.rank()), (1, 2, 1)); +} diff --git a/src/unit_tests/models/algebraic/closest_vector_problem.rs b/src/unit_tests/models/algebraic/closest_vector_problem.rs index c78712962..5d7ad48fb 100644 --- a/src/unit_tests/models/algebraic/closest_vector_problem.rs +++ b/src/unit_tests/models/algebraic/closest_vector_problem.rs @@ -29,7 +29,7 @@ fn test_cvp_evaluates_without_coefficient_bounds() { ClosestVectorProblem::new(vec![vec![2, 0, 0], vec![1, 2, 0]], vec![3_i64, 3, 1]).unwrap(); assert_eq!( problem.evaluate(&vec![1, 1]).unwrap(), - Min(Some(2.0_f64.sqrt())) + Min(Some(BigRational::from_integer(2.into()))) ); assert!(problem.evaluate(&vec![11, -12]).unwrap().0.is_some()); assert!(matches!( @@ -48,11 +48,15 @@ fn test_cvp_rejects_invalid_basis() { } #[test] -fn test_cvp_reports_rank_arithmetic_overflow() { - let error = +fn test_cvp_rank_uses_exact_integer_elimination() { + let problem = ClosestVectorProblem::new(vec![vec![i64::MAX, 1], vec![1, i64::MAX]], vec![0_i64, 0]) - .unwrap_err(); - assert!(matches!(error, ConstructionError::IntegerOverflow(_))); + .unwrap(); + assert_eq!(problem.independent_rows(), vec![0, 1]); + // Swapped pivots and a redundant ambient row preserve column rank. + let rectangular = + ClosestVectorProblem::new(vec![vec![0, 0, 1], vec![0, 1, 0]], vec![0_i64; 3]).unwrap(); + assert_eq!(rectangular.independent_rows(), vec![2, 1]); } #[test] @@ -68,16 +72,61 @@ fn test_cvp_rejects_non_finite_real_target() { } #[test] -fn test_cvp_reports_exact_to_float_boundary() { - let problem = ClosestVectorProblem::new( - vec![vec![crate::types::MAX_EXACT_F64_INTEGER + 1]], - vec![0_i64], +fn test_cvp_integer_coordinates_preserve_zero_and_unit_distance() { + let target = (1_i64 << 53) + 1; + let problem = ClosestVectorProblem::new(vec![vec![1]], vec![target]).unwrap(); + assert_eq!( + crate::solvers::customized::closest_vector_problem::solve(&problem).unwrap(), + vec![target] + ); + assert_eq!( + problem.squared_distance(&[target]).unwrap(), + BigRational::zero() + ); + assert_eq!( + problem.squared_distance(&[target - 1]).unwrap(), + BigRational::from_integer(1.into()) + ); + let cancellation = ClosestVectorProblem::new( + vec![vec![i64::MAX, 1], vec![i64::MAX - 1, 1]], + vec![1_i64, 0], ) .unwrap(); - assert!(matches!( - problem.evaluate(&vec![1]), - Err(crate::traits::EvaluationError::InexactFloatConversion(_)) - )); + assert_eq!( + cancellation.squared_distance(&[1, -1]).unwrap(), + BigRational::zero() + ); +} + +#[test] +fn test_cvp_real_target_preserves_its_stored_rational_value() { + let problem = ClosestVectorProblem::new(vec![vec![1]], vec![0.25]).unwrap(); + assert_eq!( + problem.squared_distance(&[1]).unwrap(), + BigRational::new(9.into(), 16.into()) + ); + let value = problem.evaluate(&vec![1]).unwrap(); + let serialized = + crate::registry::DynProblem::evaluate_json(&problem, &serde_json::json!([1])).unwrap(); + assert_eq!( + serde_json::from_value::>(serialized).unwrap(), + value + ); + assert_eq!( + crate::registry::DynProblem::evaluate_dyn(&problem, &serde_json::json!([1])).unwrap(), + ("Min(9/16)".into(), true) + ); + let loaded = crate::registry::LoadedDynProblem::new(Box::new(problem)); + let outcome = crate::solvers::solve(&loaded, crate::solvers::SolverRequest::Default) + .unwrap() + .outcome; + assert_eq!( + outcome, + crate::solvers::SolveOutcome::Optimal { + solution: serde_json::json!([0]), + evaluation: "Min(1/16)".into(), + } + ); } #[test] @@ -132,5 +181,8 @@ fn test_cvp_registers_both_target_variants() { #[test] fn test_cvp_empty_basis_is_valid() { let problem = ClosestVectorProblem::new(Vec::new(), vec![3_i64, 4]).unwrap(); - assert_eq!(problem.evaluate(&Vec::new()).unwrap(), Min(Some(5.0))); + assert_eq!( + problem.evaluate(&Vec::new()).unwrap(), + Min(Some(BigRational::from_integer(25.into()))) + ); } diff --git a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs index 92ae4a385..8b844199e 100644 --- a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs +++ b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs @@ -22,12 +22,16 @@ fn test_consecutive_block_minimization_basic() { let problem = ConsecutiveBlockMinimization::new( vec![vec![true, false, true], vec![false, true, true]], 2, - ); + ) + .unwrap(); assert_eq!(problem.num_rows(), 2); assert_eq!(problem.num_cols(), 3); assert_eq!(problem.bound(), 2); - assert_eq!(problem.num_variables(), 3); - assert_eq!(problem.dimensions(), vec![3; 3]); + assert_eq!(problem.num_variables().unwrap(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 3] + ); } #[test] @@ -42,7 +46,8 @@ fn test_consecutive_block_minimization_evaluate() { let problem = ConsecutiveBlockMinimization::new( vec![vec![true, false, true], vec![false, true, true]], 2, - ); + ) + .unwrap(); assert!(problem.evaluate(&vec![0, 2, 1]).unwrap()); // Identity permutation [0, 1, 2]: @@ -57,7 +62,8 @@ fn test_consecutive_block_minimization_count_blocks() { let problem = ConsecutiveBlockMinimization::new( vec![vec![true, false, true], vec![false, true, true]], 2, - ); + ) + .unwrap(); assert_eq!( problem.count_consecutive_blocks(&[0, 2, 1]).unwrap(), Some(2) @@ -79,7 +85,8 @@ fn test_consecutive_block_minimization_brute_force() { let problem = ConsecutiveBlockMinimization::new( vec![vec![true, false, true], vec![false, true, true]], 2, - ); + ) + .unwrap(); let solver = BruteForce::new(); let mut solutions = solver.find_all_witnesses(&problem).unwrap(); solutions.sort(); @@ -93,7 +100,7 @@ fn test_consecutive_block_minimization_brute_force() { #[test] fn test_consecutive_block_minimization_empty_matrix() { - let problem = ConsecutiveBlockMinimization::new(vec![], 0); + let problem = ConsecutiveBlockMinimization::new(vec![], 0).unwrap(); assert_eq!(problem.num_rows(), 0); assert_eq!(problem.num_cols(), 0); assert!(problem.evaluate(&vec![]).unwrap()); @@ -105,7 +112,8 @@ fn test_consecutive_block_minimization_empty_matrix() { #[test] fn test_consecutive_block_minimization_serialization() { - let problem = ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2); + let problem = + ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2).unwrap(); let json = serde_json::to_string(&problem).unwrap(); let deserialized: ConsecutiveBlockMinimization = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.num_rows(), problem.num_rows()); @@ -116,7 +124,8 @@ fn test_consecutive_block_minimization_serialization() { #[test] fn test_consecutive_block_minimization_serialization_omits_derived_fields() { - let problem = ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2); + let problem = + ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2).unwrap(); let value: serde_json::Value = serde_json::to_value(&problem).unwrap(); let obj = value.as_object().unwrap(); assert_eq!(obj.len(), 2); @@ -133,7 +142,8 @@ fn test_consecutive_block_minimization_deserialization_rejects_ragged_matrix() { #[test] fn test_consecutive_block_minimization_invalid_permutation() { - let problem = ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2); + let problem = + ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2).unwrap(); // Not a valid permutation => evaluate returns false assert!(!problem.evaluate(&vec![0, 0]).unwrap()); // Wrong length 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 b3cce7057..dc1f27d42 100644 --- a/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs +++ b/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs @@ -43,8 +43,11 @@ fn test_consecutive_ones_matrix_augmentation_basic() { assert_eq!(problem.num_rows(), 4); assert_eq!(problem.num_cols(), 5); assert_eq!(problem.bound(), 2); - assert_eq!(problem.num_variables(), 5); - assert_eq!(problem.dimensions(), vec![5; 5]); + assert_eq!(problem.num_variables().unwrap(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 5] + ); assert_eq!( ::NAME, "ConsecutiveOnesMatrixAugmentation" diff --git a/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs b/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs index bc20d02df..57110f99a 100644 --- a/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs +++ b/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; /// Tucker matrix (3×4) — the classic C1P obstruction. @@ -14,11 +13,14 @@ fn tucker_matrix() -> Vec> { #[test] fn test_consecutive_ones_submatrix_basic() { - let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3); + let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3).unwrap(); assert_eq!(problem.num_rows(), 3); assert_eq!(problem.num_cols(), 4); assert_eq!(problem.bound(), 3); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!( ::NAME, "ConsecutiveOnesSubmatrix" @@ -28,7 +30,7 @@ fn test_consecutive_ones_submatrix_basic() { #[test] fn test_consecutive_ones_submatrix_evaluate_satisfying() { - let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3); + let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3).unwrap(); // Select columns {0, 1, 3} → config [1, 1, 0, 1] // Permutation [1, 0, 3]: // r1: 1, 1, 1 → consecutive @@ -39,14 +41,14 @@ fn test_consecutive_ones_submatrix_evaluate_satisfying() { #[test] fn test_consecutive_ones_submatrix_evaluate_unsatisfying() { - let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 4); + let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 4).unwrap(); // Full Tucker matrix does NOT have C1P assert!(!problem.evaluate(&vec![true, true, true, true]).unwrap()); } #[test] fn test_consecutive_ones_submatrix_evaluate_wrong_count() { - let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3); + let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3).unwrap(); // Selecting 2 columns instead of 3 → false assert!(!problem.evaluate(&vec![true, true, false, false]).unwrap()); // Selecting 4 columns instead of 3 → false @@ -55,7 +57,7 @@ fn test_consecutive_ones_submatrix_evaluate_wrong_count() { #[test] fn test_consecutive_ones_submatrix_evaluate_wrong_config_length() { - let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3); + let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3).unwrap(); assert!(matches!( problem.evaluate(&vec![true, false]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -68,7 +70,7 @@ fn test_consecutive_ones_submatrix_evaluate_wrong_config_length() { #[test] fn test_consecutive_ones_submatrix_evaluate_invalid_variable_value() { - let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3); + let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3).unwrap(); assert!(crate::registry::DynProblem::evaluate_dyn( &problem, &serde_json::json!([2, false, false, true]) @@ -78,7 +80,7 @@ fn test_consecutive_ones_submatrix_evaluate_invalid_variable_value() { #[test] fn test_consecutive_ones_submatrix_brute_force() { - let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3); + let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3).unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -89,7 +91,7 @@ fn test_consecutive_ones_submatrix_brute_force() { #[test] fn test_consecutive_ones_submatrix_brute_force_all() { - let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3); + let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -101,7 +103,7 @@ fn test_consecutive_ones_submatrix_brute_force_all() { #[test] 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 problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 4).unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); } @@ -114,7 +116,7 @@ fn test_consecutive_ones_submatrix_trivial_c1p() { vec![false, true, true], vec![true, false, false], ]; - let problem = ConsecutiveOnesSubmatrix::new(matrix, 3); + let problem = ConsecutiveOnesSubmatrix::new(matrix, 3).unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -127,7 +129,7 @@ fn test_consecutive_ones_submatrix_trivial_c1p() { fn test_consecutive_ones_submatrix_single_column() { // Any single column trivially has C1P let matrix = vec![vec![true, false, true], vec![false, true, false]]; - let problem = ConsecutiveOnesSubmatrix::new(matrix, 1); + let problem = ConsecutiveOnesSubmatrix::new(matrix, 1).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert_eq!(solutions.len(), 3); // each column works individually @@ -141,7 +143,7 @@ fn test_consecutive_ones_submatrix_empty_rows() { vec![true, true, true], vec![true, false, true], ]; - let problem = ConsecutiveOnesSubmatrix::new(matrix, 2); + let problem = ConsecutiveOnesSubmatrix::new(matrix, 2).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -152,7 +154,7 @@ fn test_consecutive_ones_submatrix_empty_rows() { #[test] fn test_consecutive_ones_submatrix_serialization() { - let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3); + let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3).unwrap(); let json = serde_json::to_value(&problem).unwrap(); assert_eq!( json, @@ -174,7 +176,7 @@ fn test_consecutive_ones_submatrix_serialization() { #[test] fn test_consecutive_ones_submatrix_paper_example() { // Tucker matrix with K=3: same instance as paper - let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3); + let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3).unwrap(); // Verify that selecting cols {0,1,3} is satisfying assert!(problem.evaluate(&vec![true, true, false, true]).unwrap()); @@ -193,19 +195,22 @@ fn test_consecutive_ones_submatrix_paper_example() { fn test_consecutive_ones_submatrix_k_zero() { // K=0: empty selection always satisfies (vacuously true) let matrix = vec![vec![true, false], vec![false, true]]; - let problem = ConsecutiveOnesSubmatrix::new(matrix, 0); + let problem = ConsecutiveOnesSubmatrix::new(matrix, 0).unwrap(); assert!(problem.evaluate(&vec![false, false]).unwrap()); // select nothing assert!(!problem.evaluate(&vec![true, false]).unwrap()); // selected 1, need 0 } #[test] fn test_consecutive_ones_submatrix_empty_matrix_vacuous_case() { - let problem = ConsecutiveOnesSubmatrix::new(vec![], 0); + let problem = ConsecutiveOnesSubmatrix::new(vec![], 0).unwrap(); assert!(problem.matrix().is_empty()); assert_eq!(problem.num_rows(), 0); assert_eq!(problem.num_cols(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } @@ -221,15 +226,21 @@ fn test_consecutive_ones_submatrix_complexity_metadata_matches_evaluator() { } #[test] -#[should_panic(expected = "bound")] fn test_consecutive_ones_submatrix_k_too_large() { let matrix = vec![vec![true, false]]; - ConsecutiveOnesSubmatrix::new(matrix, 3); + assert!(ConsecutiveOnesSubmatrix::new(matrix, 3).is_err()); } #[test] -#[should_panic(expected = "same length")] fn test_consecutive_ones_submatrix_inconsistent_rows() { let matrix = vec![vec![true, false], vec![true]]; - ConsecutiveOnesSubmatrix::new(matrix, 1); + assert!(ConsecutiveOnesSubmatrix::new(matrix, 1).is_err()); +} + +#[test] +fn json_rejects_invalid_instance() { + assert!(serde_json::from_value::( + serde_json::json!({"matrix":[[true]],"bound":2}) + ) + .is_err()); } diff --git a/src/unit_tests/models/algebraic/equilibrium_point.rs b/src/unit_tests/models/algebraic/equilibrium_point.rs index dd30d1d37..2ab175c18 100644 --- a/src/unit_tests/models/algebraic/equilibrium_point.rs +++ b/src/unit_tests/models/algebraic/equilibrium_point.rs @@ -51,8 +51,11 @@ fn test_equilibrium_point_creation_and_accessors() { assert_eq!(p.range_sets()[0], vec![0, 1]); assert_eq!(p.range_sets()[1], vec![0, 1]); assert_eq!(p.range_sets()[2], vec![0, 1]); - assert_eq!(p.dimensions(), vec![2, 2, 2]); - assert_eq!(p.num_variables(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] + ); + assert_eq!(p.num_variables().unwrap(), 3); assert_eq!(::NAME, "EquilibriumPoint"); assert_eq!(::variant(), vec![]); } diff --git a/src/unit_tests/models/algebraic/feasible_basis_extension.rs b/src/unit_tests/models/algebraic/feasible_basis_extension.rs index 489d91788..4b8241a6d 100644 --- a/src/unit_tests/models/algebraic/feasible_basis_extension.rs +++ b/src/unit_tests/models/algebraic/feasible_basis_extension.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_validates_matrix_shape() { @@ -33,6 +32,7 @@ fn issue_example() -> FeasibleBasisExtension { vec![7, 5, 3], vec![0, 1], ) + .unwrap() } #[test] @@ -41,7 +41,10 @@ fn test_feasible_basis_extension_creation() { assert_eq!(problem.num_rows(), 3); assert_eq!(problem.num_columns(), 6); assert_eq!(problem.num_required(), 2); - assert_eq!(problem.dimensions(), vec![2; 4]); // 6 - 2 = 4 free columns + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); // 6 - 2 = 4 free columns assert_eq!( ::NAME, "FeasibleBasisExtension" @@ -145,7 +148,8 @@ fn test_feasible_basis_extension_unsatisfiable() { // B={0,2}: solve [[1,1],[0,-1]]x=[1,-1] => x=(0,1), x>=0 => feasible! // Let's try: A = [[1,1,1],[1,1,1]], rhs = [1,1]. All 2x2 submatrices are singular. let problem = - FeasibleBasisExtension::new(vec![vec![1, 1, 1], vec![1, 1, 1]], vec![1, 1], vec![]); + FeasibleBasisExtension::new(vec![vec![1, 1, 1], vec![1, 1, 1]], vec![1, 1], vec![]) + .unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); } @@ -197,48 +201,51 @@ fn test_feasible_basis_extension_complexity_metadata() { } #[test] -#[should_panic(expected = "must be less than")] fn test_feasible_basis_extension_m_ge_n() { // 3x3 matrix: m not < n - FeasibleBasisExtension::new( + assert!(FeasibleBasisExtension::new( vec![vec![1, 0, 0], vec![0, 1, 0], vec![0, 0, 1]], vec![1, 1, 1], vec![], - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "rhs length")] fn test_feasible_basis_extension_rhs_length_mismatch() { - FeasibleBasisExtension::new( + assert!(FeasibleBasisExtension::new( vec![vec![1, 0, 1], vec![0, 1, 0]], vec![1, 2, 3], // length 3, but m=2 vec![], - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "|S|")] fn test_feasible_basis_extension_too_many_required() { // m=2, |S|=2 is not < m - FeasibleBasisExtension::new(vec![vec![1, 0, 1], vec![0, 1, 0]], vec![1, 2], vec![0, 1]); + assert!(FeasibleBasisExtension::new( + vec![vec![1, 0, 1], vec![0, 1, 0]], + vec![1, 2], + vec![0, 1] + ) + .is_err()); } #[test] -#[should_panic(expected = "out of bounds")] fn test_feasible_basis_extension_required_out_of_bounds() { - FeasibleBasisExtension::new( + assert!(FeasibleBasisExtension::new( vec![vec![1, 0, 1], vec![0, 1, 0]], vec![1, 2], vec![5], // out of bounds - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "Duplicate")] fn test_feasible_basis_extension_duplicate_required() { // 3x5 matrix so |S|=2 < m=3, but S has duplicates - FeasibleBasisExtension::new( + assert!(FeasibleBasisExtension::new( vec![ vec![1, 0, 1, 0, 1], vec![0, 1, 0, 1, 0], @@ -246,5 +253,14 @@ fn test_feasible_basis_extension_duplicate_required() { ], vec![1, 2, 3], vec![0, 0], - ); + ) + .is_err()); +} + +#[test] +fn json_rejects_invalid_instance() { + assert!(serde_json::from_value::( + serde_json::json!({"matrix":[[1]],"rhs":[1],"required_columns":[]}) + ) + .is_err()); } diff --git a/src/unit_tests/models/algebraic/ilp.rs b/src/unit_tests/models/algebraic/ilp.rs index 64bdc8143..01e00e9de 100644 --- a/src/unit_tests/models/algebraic/ilp.rs +++ b/src/unit_tests/models/algebraic/ilp.rs @@ -54,7 +54,10 @@ fn float_constraints_use_float_arithmetic() { ) .unwrap(); - assert!(ilp.is_feasible(&[1, 1]).unwrap()); + assert!(!ilp.is_feasible(&[1, 1]).unwrap()); + assert!(LinearConstraint::eq(vec![(0, 0.1), (1, 0.2)], 0.1 + 0.2) + .is_satisfied(&[1, 1]) + .unwrap()); assert_eq!(ilp.evaluate_objective(&[1, 0]).unwrap(), 0.5); } diff --git a/src/unit_tests/models/algebraic/minimum_matrix_cover.rs b/src/unit_tests/models/algebraic/minimum_matrix_cover.rs index 89eb9ad5f..41a312085 100644 --- a/src/unit_tests/models/algebraic/minimum_matrix_cover.rs +++ b/src/unit_tests/models/algebraic/minimum_matrix_cover.rs @@ -12,11 +12,14 @@ fn test_minimum_matrix_cover_creation() { vec![1, 0, 0, 4], vec![0, 2, 4, 0], ]; - let problem = MinimumMatrixCover::new(matrix.clone()); + let problem = MinimumMatrixCover::new(matrix.clone()).unwrap(); assert_eq!(problem.num_rows(), 4); assert_eq!(problem.matrix(), &matrix); - assert_eq!(problem.dimensions(), vec![2; 4]); - assert_eq!(problem.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] @@ -29,7 +32,7 @@ fn test_minimum_matrix_cover_evaluate_all_minus() { vec![1, 0, 0, 4], vec![0, 2, 4, 0], ]; - let problem = MinimumMatrixCover::new(matrix); + let problem = MinimumMatrixCover::new(matrix).unwrap(); let value = problem.evaluate(&vec![false, false, false, false]).unwrap(); // Sum of all entries = 0+3+1+0 + 3+0+0+2 + 1+0+0+4 + 0+2+4+0 = 20 assert_eq!(value, Min(Some(20))); @@ -43,7 +46,7 @@ fn test_minimum_matrix_cover_evaluate_mixed() { vec![1, 0, 0, 4], vec![0, 2, 4, 0], ]; - let problem = MinimumMatrixCover::new(matrix); + let problem = MinimumMatrixCover::new(matrix).unwrap(); // Config [0,1,1,0] → f=(-1,+1,+1,-1) // Compute: Σ a_ij * f(i) * f(j) @@ -64,7 +67,7 @@ fn test_minimum_matrix_cover_evaluate_mixed() { #[test] fn test_minimum_matrix_cover_evaluate_invalid() { - let problem = MinimumMatrixCover::new(vec![vec![0, 1], vec![1, 0]]); + let problem = MinimumMatrixCover::new(vec![vec![0, 1], vec![1, 0]]).unwrap(); // Wrong length assert!(matches!( @@ -86,7 +89,7 @@ fn test_minimum_matrix_cover_solver() { vec![1, 0, 0, 4], vec![0, 2, 4, 0], ]; - let problem = MinimumMatrixCover::new(matrix); + let problem = MinimumMatrixCover::new(matrix).unwrap(); let solver = BruteForce::new(); let value_solution = solver.solve(&problem).unwrap().unwrap(); @@ -103,7 +106,7 @@ fn test_minimum_matrix_cover_solver() { #[test] fn test_minimum_matrix_cover_serialization() { let matrix = vec![vec![0, 1], vec![1, 0]]; - let problem = MinimumMatrixCover::new(matrix); + let problem = MinimumMatrixCover::new(matrix).unwrap(); let json = serde_json::to_string(&problem).unwrap(); let deserialized: MinimumMatrixCover = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.num_rows(), 2); @@ -114,7 +117,7 @@ fn test_minimum_matrix_cover_serialization() { fn test_minimum_matrix_cover_1x1() { // 1×1 matrix: only one variable, f(1) = ±1 // value = a_11 * f(1)^2 = a_11 regardless of sign - let problem = MinimumMatrixCover::new(vec![vec![5]]); + let problem = MinimumMatrixCover::new(vec![vec![5]]).unwrap(); assert_eq!(problem.evaluate(&vec![false]).unwrap(), Min(Some(5))); assert_eq!(problem.evaluate(&vec![true]).unwrap(), Min(Some(5))); @@ -136,7 +139,7 @@ fn test_minimum_matrix_cover_paper_example() { vec![1, 0, 0, 4], vec![0, 2, 4, 0], ]; - let problem = MinimumMatrixCover::new(matrix); + let problem = MinimumMatrixCover::new(matrix).unwrap(); let solver = BruteForce::new(); // Verify the claimed optimal from the issue @@ -167,3 +170,17 @@ fn test_minimum_matrix_cover_canonical_example_spec() { serde_json::json!([false, true, true, false]) ); } + +#[test] +fn construction_and_deserialization_enforce_nonnegative_square_matrices() { + for matrix in [vec![vec![-1]], vec![vec![0, 1]], vec![vec![0, 1], vec![1]]] { + assert!(matches!( + MinimumMatrixCover::new(matrix.clone()), + Err(ConstructionError::InvalidInput(_)) + )); + assert!(serde_json::from_value::( + serde_json::json!({"matrix": matrix}) + ) + .is_err()); + } +} diff --git a/src/unit_tests/models/algebraic/minimum_matrix_domination.rs b/src/unit_tests/models/algebraic/minimum_matrix_domination.rs index 7e31fa47e..2875b85a4 100644 --- a/src/unit_tests/models/algebraic/minimum_matrix_domination.rs +++ b/src/unit_tests/models/algebraic/minimum_matrix_domination.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -18,11 +17,14 @@ fn p6_adjacency_matrix() -> Vec> { #[test] fn test_minimum_matrix_domination_creation() { - let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()); + let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()).unwrap(); assert_eq!(problem.num_rows(), 6); assert_eq!(problem.num_cols(), 6); assert_eq!(problem.num_ones(), 10); - assert_eq!(problem.dimensions(), vec![2; 10]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 10] + ); assert_eq!( ::NAME, "MinimumMatrixDomination" @@ -32,7 +34,7 @@ fn test_minimum_matrix_domination_creation() { #[test] fn test_minimum_matrix_domination_ones_enumeration() { - let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()); + let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()).unwrap(); let expected_ones = vec![ (0, 1), (1, 0), @@ -50,7 +52,7 @@ fn test_minimum_matrix_domination_ones_enumeration() { #[test] fn test_minimum_matrix_domination_evaluate_optimal() { - let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()); + let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()).unwrap(); // Select entries 0,1,6,7: (0,1),(1,0),(3,4),(4,3) // Covered rows: {0,1,3,4}, covered cols: {0,1,3,4} // Unselected: (1,2) row 1 covered, (2,1) col 1 covered, (2,3) col 3 covered, @@ -63,7 +65,7 @@ fn test_minimum_matrix_domination_evaluate_optimal() { #[test] fn test_minimum_matrix_domination_evaluate_infeasible() { - let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()); + let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()).unwrap(); // Select only entry 0: (0,1) — covers row 0, col 1 // Entry (2,3) at index 4: row 2 not covered, col 3 not covered → infeasible let config = vec![ @@ -74,14 +76,14 @@ fn test_minimum_matrix_domination_evaluate_infeasible() { #[test] fn test_minimum_matrix_domination_evaluate_all_selected() { - let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()); + let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()).unwrap(); let config = vec![true; 10]; assert_eq!(problem.evaluate(&config).unwrap(), Min(Some(10))); } #[test] fn test_minimum_matrix_domination_evaluate_wrong_length() { - let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()); + let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()).unwrap(); assert!(matches!( problem.evaluate(&vec![true, false]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -94,7 +96,7 @@ fn test_minimum_matrix_domination_evaluate_wrong_length() { #[test] fn test_minimum_matrix_domination_evaluate_invalid_variable() { - let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()); + let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()).unwrap(); assert!(crate::registry::DynProblem::evaluate_dyn( &problem, &serde_json::json!([2, 0, 0, 0, 0, 0, 0, 0, 0, 0]) @@ -104,7 +106,7 @@ fn test_minimum_matrix_domination_evaluate_invalid_variable() { #[test] fn test_minimum_matrix_domination_brute_force() { - let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()); + let problem = MinimumMatrixDomination::new(p6_adjacency_matrix()).unwrap(); let solver = BruteForce::new(); let witness = solver .solve(&problem) @@ -123,7 +125,7 @@ fn test_minimum_matrix_domination_identity_matrix() { vec![false, true, false], vec![false, false, true], ]; - let problem = MinimumMatrixDomination::new(matrix); + let problem = MinimumMatrixDomination::new(matrix).unwrap(); assert_eq!(problem.num_ones(), 3); let solver = BruteForce::new(); let witness = solver @@ -138,7 +140,7 @@ fn test_minimum_matrix_domination_identity_matrix() { fn test_minimum_matrix_domination_single_row() { // One row with multiple ones: selecting any one dominates all others let matrix = vec![vec![true, true, true]]; - let problem = MinimumMatrixDomination::new(matrix); + let problem = MinimumMatrixDomination::new(matrix).unwrap(); assert_eq!(problem.num_ones(), 3); let solver = BruteForce::new(); let witness = solver @@ -150,9 +152,12 @@ fn test_minimum_matrix_domination_single_row() { #[test] fn test_minimum_matrix_domination_empty_matrix() { - let problem = MinimumMatrixDomination::new(vec![]); + let problem = MinimumMatrixDomination::new(vec![]).unwrap(); assert_eq!(problem.num_ones(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); // Empty config: vacuously valid with 0 selected assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } @@ -160,7 +165,7 @@ fn test_minimum_matrix_domination_empty_matrix() { #[test] fn test_minimum_matrix_domination_no_ones() { let matrix = vec![vec![false, false], vec![false, false]]; - let problem = MinimumMatrixDomination::new(matrix); + let problem = MinimumMatrixDomination::new(matrix).unwrap(); assert_eq!(problem.num_ones(), 0); assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } @@ -168,7 +173,7 @@ fn test_minimum_matrix_domination_no_ones() { #[test] fn test_minimum_matrix_domination_serialization() { let matrix = vec![vec![true, false], vec![false, true]]; - let problem = MinimumMatrixDomination::new(matrix); + let problem = MinimumMatrixDomination::new(matrix).unwrap(); let json = serde_json::to_value(&problem).unwrap(); assert_eq!( json, @@ -195,8 +200,24 @@ fn test_minimum_matrix_domination_complexity_metadata() { } #[test] -#[should_panic(expected = "same length")] fn test_minimum_matrix_domination_inconsistent_rows() { let matrix = vec![vec![true, false], vec![true]]; - MinimumMatrixDomination::new(matrix); + assert!(MinimumMatrixDomination::new(matrix).is_err()); +} + +#[test] +fn json_rejects_invalid_instance() { + assert!(serde_json::from_value::( + serde_json::json!({"matrix":[[true],[]]}) + ) + .is_err()); +} + +#[test] +fn deserialize_rebuilds_nonzero_positions() { + let model: MinimumMatrixDomination = serde_json::from_value(serde_json::json!({ + "matrix": [[true, false], [false, true]], "ones": [[99, 99]] + })) + .unwrap(); + assert_eq!(model.ones(), &[(0, 0), (1, 1)]); } diff --git a/src/unit_tests/models/algebraic/minimum_weight_decoding.rs b/src/unit_tests/models/algebraic/minimum_weight_decoding.rs index 352d1b2f8..f36c5a679 100644 --- a/src/unit_tests/models/algebraic/minimum_weight_decoding.rs +++ b/src/unit_tests/models/algebraic/minimum_weight_decoding.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_maps_rhs_to_target() { @@ -22,7 +21,7 @@ fn example_instance() -> MinimumWeightDecoding { vec![true, true, false, true], ]; let target = vec![true, true, false]; - MinimumWeightDecoding::new(matrix, target) + MinimumWeightDecoding::new(matrix, target).unwrap() } #[test] @@ -30,7 +29,10 @@ fn test_minimum_weight_decoding_creation() { let problem = example_instance(); assert_eq!(problem.num_rows(), 3); assert_eq!(problem.num_cols(), 4); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!( ::NAME, "MinimumWeightDecoding" @@ -141,7 +143,7 @@ fn test_minimum_weight_decoding_zero_syndrome() { // s = [0,0] → x = [0,0,0] is feasible with weight 0 let matrix = vec![vec![true, false, true], vec![false, true, true]]; let target = vec![false, false]; - let problem = MinimumWeightDecoding::new(matrix, target); + let problem = MinimumWeightDecoding::new(matrix, target).unwrap(); let config = vec![false, false, false]; assert_eq!(problem.evaluate(&config).unwrap(), Min(Some(0))); } @@ -158,21 +160,26 @@ fn test_minimum_weight_decoding_complexity_metadata() { } #[test] -#[should_panic(expected = "at least one row")] fn test_minimum_weight_decoding_empty_matrix() { - MinimumWeightDecoding::new(vec![], vec![]); + assert!(MinimumWeightDecoding::new(vec![], vec![]).is_err()); } #[test] -#[should_panic(expected = "same length")] fn test_minimum_weight_decoding_inconsistent_rows() { let matrix = vec![vec![true, false], vec![true]]; - MinimumWeightDecoding::new(matrix, vec![true, false]); + assert!(MinimumWeightDecoding::new(matrix, vec![true, false]).is_err()); } #[test] -#[should_panic(expected = "Target length")] fn test_minimum_weight_decoding_target_mismatch() { let matrix = vec![vec![true, false], vec![false, true]]; - MinimumWeightDecoding::new(matrix, vec![true]); + assert!(MinimumWeightDecoding::new(matrix, vec![true]).is_err()); +} + +#[test] +fn json_rejects_invalid_instance() { + assert!(serde_json::from_value::( + serde_json::json!({"matrix":[[true]],"target":[]}) + ) + .is_err()); } diff --git a/src/unit_tests/models/algebraic/minimum_weight_solution_to_linear_equations.rs b/src/unit_tests/models/algebraic/minimum_weight_solution_to_linear_equations.rs index b4603a584..c6c9e4103 100644 --- a/src/unit_tests/models/algebraic/minimum_weight_solution_to_linear_equations.rs +++ b/src/unit_tests/models/algebraic/minimum_weight_solution_to_linear_equations.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_rhs_length_mismatch() { @@ -19,7 +18,7 @@ use crate::types::Min; fn example_instance() -> MinimumWeightSolutionToLinearEquations { let matrix = vec![vec![1, 2, 3, 1], vec![2, 1, 1, 3]]; let rhs = vec![5, 4]; - MinimumWeightSolutionToLinearEquations::new(matrix, rhs) + MinimumWeightSolutionToLinearEquations::new(matrix, rhs).unwrap() } #[test] @@ -27,7 +26,10 @@ fn test_minimum_weight_solution_creation() { let problem = example_instance(); assert_eq!(problem.num_equations(), 2); assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!( ::NAME, "MinimumWeightSolutionToLinearEquations" @@ -109,7 +111,7 @@ fn test_minimum_weight_solution_zero_rhs() { // A = [[1,1],[2,2]], b = [0,0] — trivially consistent with 0 columns. let matrix = vec![vec![1, 1], vec![2, 2]]; let rhs = vec![0, 0]; - let problem = MinimumWeightSolutionToLinearEquations::new(matrix, rhs); + let problem = MinimumWeightSolutionToLinearEquations::new(matrix, rhs).unwrap(); let config = vec![false, false]; assert_eq!(problem.evaluate(&config).unwrap(), Min(Some(0))); } @@ -142,21 +144,28 @@ fn test_minimum_weight_solution_complexity_metadata() { } #[test] -#[should_panic(expected = "at least one row")] fn test_minimum_weight_solution_empty_matrix() { - MinimumWeightSolutionToLinearEquations::new(vec![], vec![]); + assert!(MinimumWeightSolutionToLinearEquations::new(vec![], vec![]).is_err()); } #[test] -#[should_panic(expected = "same length")] fn test_minimum_weight_solution_inconsistent_rows() { let matrix = vec![vec![1, 2], vec![3]]; - MinimumWeightSolutionToLinearEquations::new(matrix, vec![1, 2]); + assert!(MinimumWeightSolutionToLinearEquations::new(matrix, vec![1, 2]).is_err()); } #[test] -#[should_panic(expected = "RHS length")] fn test_minimum_weight_solution_rhs_mismatch() { let matrix = vec![vec![1, 2], vec![3, 4]]; - MinimumWeightSolutionToLinearEquations::new(matrix, vec![1]); + assert!(MinimumWeightSolutionToLinearEquations::new(matrix, vec![1]).is_err()); +} + +#[test] +fn json_rejects_invalid_instance() { + assert!( + serde_json::from_value::( + serde_json::json!({"matrix":[[1]],"rhs":[]}) + ) + .is_err() + ); } diff --git a/src/unit_tests/models/algebraic/quadratic_assignment.rs b/src/unit_tests/models/algebraic/quadratic_assignment.rs index 66f2de170..714319707 100644 --- a/src/unit_tests/models/algebraic/quadratic_assignment.rs +++ b/src/unit_tests/models/algebraic/quadratic_assignment.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -32,7 +31,7 @@ fn make_test_instance() -> QuadraticAssignment { vec![1, 3, 0, 4], vec![1, 4, 4, 0], ]; - QuadraticAssignment::new(cost_matrix, distance_matrix) + QuadraticAssignment::new(cost_matrix, distance_matrix).unwrap() } #[test] @@ -40,7 +39,10 @@ fn test_quadratic_assignment_creation() { let qap = make_test_instance(); assert_eq!(qap.num_facilities(), 4); assert_eq!(qap.num_locations(), 4); - assert_eq!(qap.dimensions(), vec![4, 4, 4, 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&qap).unwrap(), + vec![4, 4, 4, 4] + ); assert_eq!(qap.cost_matrix().len(), 4); assert_eq!(qap.distance_matrix().len(), 4); } @@ -118,10 +120,13 @@ fn test_quadratic_assignment_rectangular() { // 2 facilities, 3 locations (n < m) let cost_matrix = vec![vec![0, 3], vec![3, 0]]; let distance_matrix = vec![vec![0, 1, 4], vec![1, 0, 2], vec![4, 2, 0]]; - let qap = QuadraticAssignment::new(cost_matrix, distance_matrix); + let qap = QuadraticAssignment::new(cost_matrix, distance_matrix).unwrap(); assert_eq!(qap.num_facilities(), 2); assert_eq!(qap.num_locations(), 3); - assert_eq!(qap.dimensions(), vec![3, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&qap).unwrap(), + 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, &vec![0, 1]).unwrap(), Min(Some(6))); // Assignment f=(0,2): cost = 3*D[0][2] + 3*D[2][0] = 3*4 + 3*4 = 24 @@ -133,18 +138,16 @@ fn test_quadratic_assignment_rectangular() { } #[test] -#[should_panic(expected = "cost_matrix must be square")] fn test_quadratic_assignment_nonsquare_cost() { - QuadraticAssignment::new(vec![vec![0, 1]], vec![vec![0, 1], vec![1, 0]]); + assert!(QuadraticAssignment::new(vec![vec![0, 1]], vec![vec![0, 1], vec![1, 0]]).is_err()); } #[test] -#[should_panic(expected = "num_facilities")] fn test_quadratic_assignment_too_many_facilities() { - // 3 facilities, 2 locations (n > m) -- should panic + // 3 facilities, 2 locations (n > m) -- rejected let cost = vec![vec![0, 1, 2], vec![1, 0, 3], vec![2, 3, 0]]; let dist = vec![vec![0, 1], vec![1, 0]]; - QuadraticAssignment::new(cost, dist); + assert!(QuadraticAssignment::new(cost, dist).is_err()); } #[test] @@ -160,3 +163,11 @@ fn test_quadratic_assignment_solver() { Min(Some(56)) ); } + +#[test] +fn json_rejects_invalid_instance() { + assert!(serde_json::from_value::( + serde_json::json!({"cost_matrix":[[1]],"distance_matrix":[[]]}) + ) + .is_err()); +} diff --git a/src/unit_tests/models/algebraic/quadratic_congruences.rs b/src/unit_tests/models/algebraic/quadratic_congruences.rs index 41a805acc..fada632db 100644 --- a/src/unit_tests/models/algebraic/quadratic_congruences.rs +++ b/src/unit_tests/models/algebraic/quadratic_congruences.rs @@ -33,8 +33,11 @@ fn test_quadratic_congruences_creation_and_accessors() { assert_eq!(p.bit_length_b(), 4); assert_eq!(p.bit_length_c(), 4); // x is encoded as 4 binary digits because c - 1 = 9 has 4 bits. - assert_eq!(p.dimensions(), vec![2, 2, 2, 2]); - assert_eq!(p.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2, 2] + ); + assert_eq!(p.num_variables().unwrap(), 4); assert_eq!( ::NAME, "QuadraticCongruences" @@ -56,7 +59,10 @@ fn test_quadratic_congruences_evaluate_yes() { fn test_quadratic_congruences_evaluate_no() { let p = no_problem(); // c - 1 = 6 has 3 bits. - assert_eq!(p.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] + ); for x in 1..7 { // quadratic residues mod 7 are {0,1,2,4}; 3 is not one assert_eq!(p.evaluate(&config_for_x(&p, x)).unwrap(), Or(false)); @@ -74,7 +80,10 @@ fn test_quadratic_congruences_evaluate_invalid_config() { fn test_quadratic_congruences_c_le_1() { // c=1: search space {1..0} is empty let p = QuadraticCongruences::new(0, 5, 1); - assert_eq!(p.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + Vec::::new() + ); assert_eq!(p.evaluate(&BigUint::default()).unwrap(), Or(false)); assert_eq!(p.evaluate(&bu(1)).unwrap(), Or(false)); } @@ -86,7 +95,10 @@ fn test_quadratic_congruences_bigint_witness_encoding_round_trip() { let x = (BigUint::from(1u32) << 100usize) + BigUint::from(1u32); let config = p.encode_witness(&x).expect("x should be encodable"); - assert_eq!(config.len(), p.dimensions().len()); + assert_eq!( + config.len(), + crate::solvers::cartesian_dimensions(&p).unwrap().len() + ); assert_eq!(p.decode_witness(&config), Some(x)); } diff --git a/src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs b/src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs index 09d0c968b..e6d0b59ec 100644 --- a/src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs +++ b/src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs @@ -33,8 +33,11 @@ fn test_quadratic_diophantine_equations_creation_and_accessors() { assert_eq!(problem.bit_length_b(), 3); assert_eq!(problem.bit_length_c(), 6); // max_x = floor(sqrt(53 / 3)) = 4, encoded in 3 binary digits. - assert_eq!(problem.dimensions(), vec![2, 2, 2]); - assert_eq!(problem.num_variables(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2] + ); + assert_eq!(problem.num_variables().unwrap(), 3); assert_eq!( ::NAME, "QuadraticDiophantineEquations" @@ -69,7 +72,10 @@ fn test_quadratic_diophantine_equations_evaluate_yes() { #[test] fn test_quadratic_diophantine_equations_evaluate_no() { let problem = no_problem(); - assert_eq!(problem.dimensions(), vec![2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2] + ); assert_eq!( problem.evaluate(&config_for_x(&problem, 1)).unwrap(), Or(false) @@ -86,7 +92,10 @@ fn test_quadratic_diophantine_equations_evaluate_invalid_config() { #[test] fn test_quadratic_diophantine_equations_c_le_a() { let problem = QuadraticDiophantineEquations::new(10, 1, 5); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&BigUint::default()).unwrap(), Or(false)); } diff --git a/src/unit_tests/models/algebraic/qubo.rs b/src/unit_tests/models/algebraic/qubo.rs index 448fef75b..97eb6ed27 100644 --- a/src/unit_tests/models/algebraic/qubo.rs +++ b/src/unit_tests/models/algebraic/qubo.rs @@ -1,31 +1,31 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; -include!("../../jl_helpers.rs"); #[test] fn test_qubo_from_matrix() { let problem = QUBO::from_matrix(vec![vec![1, 2], vec![0, 3]]).unwrap(); assert_eq!(problem.num_vars(), 2); - assert_eq!(problem.get(0, 0), Some(&1)); - assert_eq!(problem.get(0, 1), Some(&2)); - assert_eq!(problem.get(1, 1), Some(&3)); + assert_eq!(problem.get(0, 0), Some(1)); + assert_eq!(problem.get(0, 1), Some(2)); + assert_eq!(problem.get(1, 1), Some(3)); } #[test] fn test_qubo_new() { let problem = QUBO::new(vec![1.0, 2.0], vec![((0, 1), 3.0)]).unwrap(); - assert_eq!(problem.get(0, 0), Some(&1.0)); - assert_eq!(problem.get(1, 1), Some(&2.0)); - assert_eq!(problem.get(0, 1), Some(&3.0)); + assert_eq!(problem.get(0, 0), Some(1.0)); + assert_eq!(problem.get(1, 1), Some(2.0)); + assert_eq!(problem.get(0, 1), Some(3.0)); } #[test] fn test_num_variables() { let problem = QUBO::::from_matrix(vec![vec![0.0; 5]; 5]).unwrap(); - assert_eq!(problem.num_variables(), 5); + assert_eq!(problem.num_variables().unwrap(), 5); } #[test] @@ -37,8 +37,8 @@ fn test_matrix_access() { ]) .unwrap(); let matrix = problem.matrix(); - assert_eq!(matrix.len(), 3); - assert_eq!(matrix[0], vec![1.0, 2.0, 3.0]); + assert_eq!(matrix.rows(), 3); + assert_eq!(matrix.outer_view(0).unwrap().data(), &[1.0, 2.0, 3.0]); } #[test] @@ -70,7 +70,7 @@ fn test_qubo_rejects_invalid_configurations() { fn test_qubo_new_reverse_indices() { // Test the case where (j, i) is provided with i < j let problem = QUBO::new(vec![1.0, 2.0], vec![((1, 0), 3.0)]).unwrap(); // j > i - assert_eq!(problem.get(0, 1), Some(&3.0)); // Should be stored at (0, 1) + assert_eq!(problem.get(0, 1), Some(3.0)); // Should be stored at (0, 1) } #[test] @@ -157,8 +157,8 @@ fn test_qubo_f64_create_spec() { }) .unwrap(); - assert_eq!(problem.get(0, 0), Some(&0.5)); - assert_eq!(problem.get(0, 1), Some(&-1.25)); + assert_eq!(problem.get(0, 0), Some(0.5)); + assert_eq!(problem.get(0, 1), Some(-1.25)); } #[test] @@ -172,10 +172,10 @@ fn test_qubo_rejects_non_square_matrix() { #[test] fn test_qubo_rejects_non_finite_coefficients() { - let error = QUBO::from_matrix(vec![vec![f64::NAN]]).unwrap_err(); + let error = QUBO::from_matrix(vec![vec![0.0, f64::NAN], vec![0.0, 0.0]]).unwrap_err(); assert!(matches!( error, - crate::registry::ConstructionError::NonFiniteFloat(_) + crate::registry::ConstructionError::NonFiniteFloat(message) if message.contains("(0, 1)") )); let error = QUBO::new(vec![f64::INFINITY], vec![]).unwrap_err(); assert!(matches!( @@ -202,3 +202,81 @@ fn test_integer_qubo_reports_objective_overflow() { Err(crate::traits::EvaluationError::IntegerOverflow(_)) )); } + +#[test] +fn sparse_storage_preserves_every_assignment_and_sum_order() { + let integer = vec![ + vec![3, -5, 0, 2], + vec![99, 0, 7, -4], + vec![0, 0, -6, 0], + vec![0, 0, 0, 1], + ]; + let floating = vec![ + vec![1e16, 1.0, -1e16, 0.0], + vec![99.0, 0.5, 0.0, -0.25], + vec![0.0, 0.0, -2.0, 0.0], + vec![0.0, 0.0, 0.0, 1.0], + ]; + let int_problem = QUBO::from_matrix(integer.clone()).unwrap(); + let float_problem = QUBO::from_matrix(floating.clone()).unwrap(); + for mask in 0..16 { + let solution: Vec = (0..4).map(|i| mask & (1 << i) != 0).collect(); + let mut int_value = 0i64; + let mut float_value = 0.0f64; + for i in 0..4 { + for j in i..4 { + if solution[i] && solution[j] { + int_value = int_value.checked_add(integer[i][j]).unwrap(); + float_value += floating[i][j]; + } + } + } + assert_eq!( + int_problem.evaluate(&solution).unwrap(), + Min(Some(int_value)) + ); + assert_eq!( + float_problem + .evaluate(&solution) + .unwrap() + .unwrap() + .to_bits(), + float_value.to_bits() + ); + } +} + +#[test] +fn sparse_qubo_keeps_unused_variables_and_last_assignment() { + let problem = QUBO::new( + vec![0i64; 10_000], + vec![((2, 7), i64::MAX), ((7, 2), 5), ((9, 9), 3), ((9, 9), 0)], + ) + .unwrap(); + assert_eq!(problem.num_vars(), 10_000); + assert_eq!(problem.matrix().nnz(), 1); + assert_eq!(problem.get(9, 9), Some(0)); + assert_eq!(problem.get(2, 7), Some(5)); + let json = serde_json::to_string(&problem).unwrap(); + assert!(json.len() < 100_000); + let restored: QUBO = serde_json::from_str(&json).unwrap(); + assert_eq!(restored.matrix(), problem.matrix()); + let mut solution = vec![false; 10_000]; + solution[2] = true; + solution[7] = true; + assert_eq!(restored.evaluate(&solution).unwrap(), Min(Some(5))); +} + +#[test] +fn sparse_qubo_validates_shape_values_and_serialized_structure() { + assert!(QUBO::from_sparse(CsMat::::zero((2, 3))).is_err()); + let invalid = CsMat::new((1, 1), vec![0, 1], vec![0], vec![f64::INFINITY]); + assert!(QUBO::from_sparse(invalid).is_err()); + let column_matrix = CsMat::new_csc((2, 2), vec![0, 1, 2], vec![0, 0], vec![2i64, 3]); + let problem = QUBO::from_sparse(column_matrix).unwrap(); + assert!(problem.matrix().is_csr()); + assert_eq!(problem.evaluate(&vec![true, true]).unwrap(), Min(Some(5))); + let mut json = serde_json::to_value(&problem).unwrap(); + json["matrix"]["indptr"] = serde_json::json!([0, 3, 2]); + assert!(serde_json::from_value::>(json).is_err()); +} diff --git a/src/unit_tests/models/algebraic/simultaneous_incongruences.rs b/src/unit_tests/models/algebraic/simultaneous_incongruences.rs index 0506d3829..d7a3edff6 100644 --- a/src/unit_tests/models/algebraic/simultaneous_incongruences.rs +++ b/src/unit_tests/models/algebraic/simultaneous_incongruences.rs @@ -25,9 +25,9 @@ fn test_simultaneous_incongruences_creation_and_accessors() { assert_eq!(p.num_pairs(), 4); assert_eq!(p.pairs(), &[(2, 2), (1, 3), (2, 5), (3, 7)]); // lcm(2,3,5,7) = 210 - assert_eq!(p.lcm_moduli(), 210); - assert_eq!(p.dimensions(), vec![210]); - assert_eq!(p.num_variables(), 1); + assert_eq!(p.lcm_moduli().unwrap(), 210); + assert_eq!(crate::solvers::cartesian_dimensions(&p).unwrap(), vec![210]); + assert_eq!(p.num_variables().unwrap(), 1); assert_eq!( ::NAME, "SimultaneousIncongruences" @@ -49,7 +49,7 @@ fn test_simultaneous_incongruences_evaluate_no() { let p = covering_system(); // pairs (2,2) and (1,2): together require x≡0 (mod 2) AND x≡1 (mod 2), // which is impossible. - let lcm = p.lcm_moduli(); + let lcm = p.lcm_moduli().unwrap(); assert_eq!(lcm, 2); // All x in {0,1} should fail for x in 0..lcm { @@ -72,8 +72,8 @@ fn test_simultaneous_incongruences_evaluate_invalid_config() { fn test_simultaneous_incongruences_empty_pairs() { let p = SimultaneousIncongruences::new(vec![]).unwrap(); assert_eq!(p.num_pairs(), 0); - assert_eq!(p.lcm_moduli(), 1); - assert_eq!(p.dimensions(), vec![1]); + assert_eq!(p.lcm_moduli().unwrap(), 1); + assert_eq!(crate::solvers::cartesian_dimensions(&p).unwrap(), vec![1]); // Any x (here x=0) satisfies vacuously assert_eq!(p.evaluate(&0).unwrap(), Or(true)); } @@ -131,3 +131,19 @@ fn test_simultaneous_incongruences_paper_example() { let witness = solver.solve(&p).unwrap().unwrap(); assert_eq!(p.evaluate(&witness).unwrap(), Or(true)); } + +#[test] +fn period_overflow_does_not_restrict_model_evaluation() { + let problem = SimultaneousIncongruences::new(vec![(1, i64::MAX), (1, i64::MAX - 1)]).unwrap(); + let restored: SimultaneousIncongruences = + serde_json::from_value(serde_json::to_value(&problem).unwrap()).unwrap(); + assert_eq!(restored.evaluate(&0).unwrap(), Or(true)); + assert_eq!(restored.evaluate(&-1).unwrap(), Or(false)); + assert_eq!(restored.parameters(), problem.parameters()); + assert!(matches!( + crate::solvers::cartesian_dimensions(&restored), + Err(crate::solvers::SolveError::Evaluation( + crate::traits::EvaluationError::IntegerOverflow(_) + )) + )); +} diff --git a/src/unit_tests/models/algebraic/sparse_matrix_compression.rs b/src/unit_tests/models/algebraic/sparse_matrix_compression.rs index 7ee9784c7..f5e15081f 100644 --- a/src/unit_tests/models/algebraic/sparse_matrix_compression.rs +++ b/src/unit_tests/models/algebraic/sparse_matrix_compression.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_zero_bound() { @@ -25,14 +24,17 @@ fn issue_example_matrix() -> Vec> { #[test] fn test_sparse_matrix_compression_basic() { - let problem = SparseMatrixCompression::new(issue_example_matrix(), 2); + let problem = SparseMatrixCompression::new(issue_example_matrix(), 2).unwrap(); assert_eq!(problem.matrix(), issue_example_matrix().as_slice()); assert_eq!(problem.num_rows(), 4); assert_eq!(problem.num_cols(), 4); assert_eq!(problem.bound_k(), 2); assert_eq!(problem.storage_len(), 6); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!( ::NAME, "SparseMatrixCompression" @@ -42,7 +44,7 @@ fn test_sparse_matrix_compression_basic() { #[test] fn test_sparse_matrix_compression_issue_example_is_satisfying() { - let problem = SparseMatrixCompression::new(issue_example_matrix(), 2); + let problem = SparseMatrixCompression::new(issue_example_matrix(), 2).unwrap(); assert!(problem.evaluate(&vec![1, 1, 1, 0]).unwrap()); assert_eq!( @@ -55,7 +57,7 @@ fn test_sparse_matrix_compression_issue_example_is_satisfying() { #[test] fn test_sparse_matrix_compression_issue_unsatisfying_examples() { - let problem = SparseMatrixCompression::new(issue_example_matrix(), 2); + let problem = SparseMatrixCompression::new(issue_example_matrix(), 2).unwrap(); assert!(!problem.evaluate(&vec![0, 0, 0, 0]).unwrap()); assert!(!problem.evaluate(&vec![0, 1, 1, 1]).unwrap()); @@ -64,7 +66,7 @@ fn test_sparse_matrix_compression_issue_unsatisfying_examples() { #[test] fn test_sparse_matrix_compression_rejects_bad_configs() { - let problem = SparseMatrixCompression::new(issue_example_matrix(), 2); + let problem = SparseMatrixCompression::new(issue_example_matrix(), 2).unwrap(); assert!(matches!( problem.evaluate(&vec![1, 1, 1]), @@ -83,7 +85,7 @@ fn test_sparse_matrix_compression_rejects_bad_configs() { #[test] fn test_sparse_matrix_compression_bruteforce_finds_unique_solution() { - let problem = SparseMatrixCompression::new(issue_example_matrix(), 2); + let problem = SparseMatrixCompression::new(issue_example_matrix(), 2).unwrap(); let solver = BruteForce::new(); let solution = solver @@ -98,7 +100,7 @@ fn test_sparse_matrix_compression_bruteforce_finds_unique_solution() { #[test] fn test_sparse_matrix_compression_serialization() { - let problem = SparseMatrixCompression::new(issue_example_matrix(), 2); + let problem = SparseMatrixCompression::new(issue_example_matrix(), 2).unwrap(); let json = serde_json::to_value(&problem).unwrap(); assert_eq!( @@ -134,13 +136,19 @@ fn test_sparse_matrix_compression_complexity_metadata_matches_evaluator() { } #[test] -#[should_panic(expected = "bound_k")] fn test_sparse_matrix_compression_rejects_zero_bound() { - let _ = SparseMatrixCompression::new(issue_example_matrix(), 0); + assert!(SparseMatrixCompression::new(issue_example_matrix(), 0).is_err()); } #[test] -#[should_panic(expected = "same length")] fn test_sparse_matrix_compression_rejects_ragged_matrix() { - let _ = SparseMatrixCompression::new(vec![vec![true, false], vec![true]], 2); + assert!(SparseMatrixCompression::new(vec![vec![true, false], vec![true]], 2).is_err()); +} + +#[test] +fn json_rejects_invalid_instance() { + assert!(serde_json::from_value::( + serde_json::json!({"matrix":[[true]],"bound_k":0}) + ) + .is_err()); } diff --git a/src/unit_tests/models/decision.rs b/src/unit_tests/models/decision.rs index 9bce00a5b..7264adea8 100644 --- a/src/unit_tests/models/decision.rs +++ b/src/unit_tests/models/decision.rs @@ -1,19 +1,18 @@ use crate::models::decision::Decision; use crate::models::graph::{MaximumIndependentSet, MinimumDominatingSet, MinimumVertexCover}; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::{One, Or}; fn triangle_mvc() -> MinimumVertexCover { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - MinimumVertexCover::new(graph, vec![1; 3]) + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + MinimumVertexCover::new(graph, vec![1; 3]).unwrap() } fn star_mds() -> MinimumDominatingSet { - let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (0, 3), (0, 4)]); - MinimumDominatingSet::new(graph, vec![One; 5]) + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (0, 3), (0, 4)]).unwrap(); + MinimumDominatingSet::new(graph, vec![One; 5]).unwrap() } #[test] @@ -68,8 +67,8 @@ fn test_decision_min_evaluate_infeasible_config() { #[test] fn test_decision_max_evaluate() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); - let mis = MaximumIndependentSet::new(graph, vec![1; 4]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); + let mis = MaximumIndependentSet::new(graph, vec![1; 4]).unwrap(); let decision = Decision::new(mis, 2); assert_eq!( decision.evaluate(&vec![true, false, true, false]).unwrap(), @@ -84,7 +83,10 @@ fn test_decision_max_evaluate() { #[test] fn test_decision_dims() { let decision = Decision::new(triangle_mvc(), 2); - assert_eq!(decision.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&decision).unwrap(), + vec![2, 2, 2] + ); } #[test] @@ -303,7 +305,7 @@ fn test_decision_mis_unit_registration_and_construction() { #[test] fn test_decision_mis_unit_dynamic_identity_edges() { let decision = Decision::new( - MaximumIndependentSet::new(SimpleGraph::path(3), vec![One; 3]), + MaximumIndependentSet::new(SimpleGraph::path(3), vec![One; 3]).unwrap(), 2, ); let variant = Decision::>::variant(); @@ -319,8 +321,13 @@ fn test_decision_mis_unit_dynamic_identity_edges() { assert_eq!((edge.parameter_declarations_fn)().fields.len(), 2); let witness = vec![true, false, true]; let reduced = (edge.reduce_fn.unwrap())(&decision).unwrap(); + assert!(std::ptr::eq( + reduced.witness.target_problem_any(), + reduced.aggregate.as_ref().unwrap().target_problem_any(), + )); assert_eq!( *reduced + .witness .extract_solution_dyn(&witness) .unwrap() .downcast::>() @@ -333,12 +340,8 @@ fn test_decision_mis_unit_dynamic_identity_edges() { )); let aggregate = (edge.reduce_aggregate_fn.unwrap())(&decision).unwrap(); assert_eq!( - *aggregate - .extract_value_from_solution_dyn(&witness) - .unwrap() - .downcast::() - .unwrap(), - Or(true) + aggregate.extract_value_dyn(serde_json::json!(2)), + serde_json::json!(true) ); assert!(matches!( (edge.reduce_aggregate_fn.unwrap())(decision.inner()), @@ -356,3 +359,47 @@ fn test_decision_mis_unit_dynamic_identity_edges() { assert!(reverse.reduce_fn.is_none()); assert_eq!((reverse.parameter_declarations_fn)().fields.len(), 2); } + +#[test] +fn unit_vertex_cover_uses_registered_construction_and_solver() { + use crate::models::decision::DecisionCreateSpec; + type Unit = MinimumVertexCover; + let spec: DecisionCreateSpec = serde_json::from_value(serde_json::json!({ + "graph": {"num_vertices": 3, "edges": [[0, 1], [1, 2]]}, "bound": 1 + })) + .unwrap(); + let problem: Decision = spec.into(); + let solution = BruteForce::new().solve(&problem).unwrap().unwrap(); + assert_eq!(solution, vec![false, true, false]); + assert_eq!(problem.evaluate(&solution), Ok(Or(true))); + let restored: Decision = + serde_json::from_value(serde_json::to_value(&problem).unwrap()).unwrap(); + assert_eq!(restored.evaluate(&solution), Ok(Or(true))); + assert_eq!(problem.num_vertices(), 3); + assert_eq!(problem.num_edges(), 2); +} + +#[test] +fn decision_executed_result_maps_witness_and_bound_together() { + use crate::rules::{AggregateReductionResult, ReduceTo, ReductionResult}; + use crate::types::Min; + + let witness = vec![true, true, false]; + for bound in [1, 2] { + let decision = Decision::new(triangle_mvc(), bound); + let result = + as ReduceTo>>::reduce_to(&decision) + .unwrap(); + let target = ReductionResult::target_problem(&result); + assert!(std::ptr::eq( + target, + AggregateReductionResult::target_problem(&result), + )); + let value = target.evaluate(&witness).unwrap(); + assert_eq!(result.extract_value(value), Or(bound == 2)); + assert_eq!(result.extract_value(Min(None)), Or(false)); + if bound == 2 { + assert_eq!(result.extract_solution(&witness).unwrap(), witness); + } + } +} diff --git a/src/unit_tests/models/formula/circuit.rs b/src/unit_tests/models/formula/circuit.rs index 8e692d141..8616db9b5 100644 --- a/src/unit_tests/models/formula/circuit.rs +++ b/src/unit_tests/models/formula/circuit.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -124,7 +123,10 @@ fn test_circuit_sat_creation() { )]); let problem = CircuitSAT::new(circuit); assert_eq!(problem.num_variables(), 3); // c, x, y - assert_eq!(problem.dimensions(), vec![2, 2, 2]); // binary variables + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2] + ); // binary variables } #[test] @@ -227,7 +229,10 @@ fn test_circuit_sat_problem() { let p = CircuitSAT::new(circuit); // Variables sorted: c, x, y - assert_eq!(p.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] + ); // c=1, x=1, y=1: c = 1 AND 1 = 1 => satisfied assert!(p.evaluate(&vec![true, true, true]).unwrap()); diff --git a/src/unit_tests/models/formula/ksat.rs b/src/unit_tests/models/formula/ksat.rs index 4ec78e2a4..d2a58fbd3 100644 --- a/src/unit_tests/models/formula/ksat.rs +++ b/src/unit_tests/models/formula/ksat.rs @@ -1,9 +1,8 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::variant::{K2, K3, KN}; -include!("../../jl_helpers.rs"); #[test] fn test_3sat_creation() { @@ -129,7 +128,10 @@ fn test_ksat_problem_v2() { ], ); - assert_eq!(p.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] + ); assert!(p.evaluate(&vec![true, false, false]).unwrap()); assert!(!p.evaluate(&vec![true, true, true]).unwrap()); assert!(!p.evaluate(&vec![false, false, false]).unwrap()); @@ -146,7 +148,10 @@ fn test_ksat_problem_v2_2sat() { vec![CNFClause::new(vec![1, 2]), CNFClause::new(vec![-1, -2])], ); - assert_eq!(p.dimensions(), vec![2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2] + ); assert!(p.evaluate(&vec![true, false]).unwrap()); assert!(p.evaluate(&vec![false, true]).unwrap()); assert!(!p.evaluate(&vec![true, true]).unwrap()); diff --git a/src/unit_tests/models/formula/maximum_2_satisfiability.rs b/src/unit_tests/models/formula/maximum_2_satisfiability.rs index fec3102ca..a062b2932 100644 --- a/src/unit_tests/models/formula/maximum_2_satisfiability.rs +++ b/src/unit_tests/models/formula/maximum_2_satisfiability.rs @@ -1,7 +1,6 @@ use super::*; use crate::models::formula::CNFClause; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Max; @@ -25,7 +24,10 @@ fn test_maximum_2_satisfiability_creation() { let problem = issue_instance(); assert_eq!(problem.num_vars(), 4); assert_eq!(problem.num_clauses(), 7); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); } #[test] diff --git a/src/unit_tests/models/formula/nae_satisfiability.rs b/src/unit_tests/models/formula/nae_satisfiability.rs index 1e164b6ee..31f52e7b0 100644 --- a/src/unit_tests/models/formula/nae_satisfiability.rs +++ b/src/unit_tests/models/formula/nae_satisfiability.rs @@ -24,7 +24,7 @@ fn test_nae_satisfiability_creation() { assert_eq!(problem.num_vars(), 5); assert_eq!(problem.num_clauses(), 5); assert_eq!(problem.num_literals(), 15); - assert_eq!(problem.num_variables(), 5); + assert_eq!(problem.num_variables().unwrap(), 5); } #[test] diff --git a/src/unit_tests/models/formula/non_tautology.rs b/src/unit_tests/models/formula/non_tautology.rs index f3c41161a..d15c2d574 100644 --- a/src/unit_tests/models/formula/non_tautology.rs +++ b/src/unit_tests/models/formula/non_tautology.rs @@ -8,8 +8,11 @@ fn test_non_tautology_creation() { let problem = NonTautology::new(3, vec![vec![1, 2, 3], vec![-1, -2, -3]]).unwrap(); assert_eq!(problem.num_vars(), 3); assert_eq!(problem.num_disjuncts(), 2); - assert_eq!(problem.num_variables(), 3); - assert_eq!(problem.dimensions(), vec![2, 2, 2]); + assert_eq!(problem.num_variables().unwrap(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2] + ); } #[test] diff --git a/src/unit_tests/models/formula/one_in_three_satisfiability.rs b/src/unit_tests/models/formula/one_in_three_satisfiability.rs index 601ee47bc..fdcbcd8f3 100644 --- a/src/unit_tests/models/formula/one_in_three_satisfiability.rs +++ b/src/unit_tests/models/formula/one_in_three_satisfiability.rs @@ -15,8 +15,11 @@ fn test_one_in_three_satisfiability_creation() { ); assert_eq!(problem.num_vars(), 4); assert_eq!(problem.num_clauses(), 3); - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); } #[test] diff --git a/src/unit_tests/models/formula/planar_3_satisfiability.rs b/src/unit_tests/models/formula/planar_3_satisfiability.rs index 7ecbe2c52..eac779e21 100644 --- a/src/unit_tests/models/formula/planar_3_satisfiability.rs +++ b/src/unit_tests/models/formula/planar_3_satisfiability.rs @@ -16,8 +16,11 @@ fn test_planar_3_satisfiability_creation() { ); assert_eq!(problem.num_vars(), 4); assert_eq!(problem.num_clauses(), 4); - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); } #[test] diff --git a/src/unit_tests/models/formula/qbf.rs b/src/unit_tests/models/formula/qbf.rs index 32bee8687..cb4a35295 100644 --- a/src/unit_tests/models/formula/qbf.rs +++ b/src/unit_tests/models/formula/qbf.rs @@ -21,7 +21,7 @@ fn test_qbf_creation() { ); assert_eq!(problem.num_vars(), 3); assert_eq!(problem.num_clauses(), 2); - assert_eq!(problem.num_variables(), 0); + assert_eq!(problem.num_variables().unwrap(), 0); assert_eq!(problem.quantifiers().len(), 3); assert_eq!(problem.clauses().len(), 2); } @@ -47,7 +47,10 @@ fn test_qbf_evaluate_true() { ); // dims() is empty; evaluate([]) runs the game-tree search - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&()).unwrap()); assert!(problem.is_true()); } @@ -131,7 +134,10 @@ fn test_qbf_zero_vars() { let problem = QuantifiedBooleanFormulas::new(0, vec![], vec![]); assert!(problem.evaluate(&()).unwrap()); assert!(problem.is_true()); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); } #[test] @@ -204,7 +210,10 @@ fn test_qbf_serialization() { assert_eq!(deserialized.num_vars(), problem.num_vars()); assert_eq!(deserialized.num_clauses(), problem.num_clauses()); assert_eq!(deserialized.quantifiers(), problem.quantifiers()); - assert_eq!(deserialized.dimensions(), problem.dimensions()); + assert_eq!( + crate::solvers::cartesian_dimensions(&deserialized).unwrap(), + crate::solvers::cartesian_dimensions(&problem).unwrap() + ); } #[test] @@ -238,7 +247,10 @@ fn test_qbf_dims() { vec![CNFClause::new(vec![1, 2, 3, 4])], ); // dims() is always empty — QBF has no external config variables - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); } #[test] diff --git a/src/unit_tests/models/formula/sat.rs b/src/unit_tests/models/formula/sat.rs index 41621a4ca..006995b4b 100644 --- a/src/unit_tests/models/formula/sat.rs +++ b/src/unit_tests/models/formula/sat.rs @@ -1,8 +1,8 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; -include!("../../jl_helpers.rs"); #[test] fn test_cnf_clause_creation() { @@ -40,7 +40,7 @@ fn test_sat_creation() { ); assert_eq!(problem.num_vars(), 3); assert_eq!(problem.num_clauses(), 2); - assert_eq!(problem.num_variables(), 3); + assert_eq!(problem.num_variables().unwrap(), 3); } #[test] @@ -149,7 +149,7 @@ fn test_is_satisfying_assignment_defaults() { #[test] fn test_num_variables() { let problem = Satisfiability::new(5, vec![CNFClause::new(vec![1])]); - assert_eq!(problem.num_variables(), 5); + assert_eq!(problem.num_variables().unwrap(), 5); } #[test] diff --git a/src/unit_tests/models/graph/acyclic_partition.rs b/src/unit_tests/models/graph/acyclic_partition.rs index 75ab99132..f85ced9f8 100644 --- a/src/unit_tests/models/graph/acyclic_partition.rs +++ b/src/unit_tests/models/graph/acyclic_partition.rs @@ -20,12 +20,14 @@ fn yes_instance() -> AcyclicPartition { (3, 5), (4, 5), ], - ), + ) + .unwrap(), vec![2, 3, 2, 1, 3, 1], vec![1; 8], 5, 5, ) + .unwrap() } fn no_cost_instance() -> AcyclicPartition { @@ -42,22 +44,25 @@ fn no_cost_instance() -> AcyclicPartition { (3, 5), (4, 5), ], - ), + ) + .unwrap(), vec![2, 3, 2, 1, 3, 1], vec![1; 8], 5, 4, ) + .unwrap() } fn quotient_cycle_instance() -> AcyclicPartition { AcyclicPartition::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(), vec![1, 1, 1], vec![1, 1, 1], 3, 3, ) + .unwrap() } fn canonicalize_labels(config: &[usize]) -> Vec { @@ -81,7 +86,10 @@ fn test_acyclic_partition_creation_and_accessors() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_arcs(), 8); - assert_eq!(problem.dimensions(), vec![6; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6; 6] + ); assert_eq!(problem.graph().arcs().len(), 8); assert_eq!(problem.vertex_weights(), &[2, 3, 2, 1, 3, 1]); assert_eq!(problem.arc_costs(), &[1, 1, 1, 1, 1, 1, 1, 1]); @@ -89,8 +97,8 @@ fn test_acyclic_partition_creation_and_accessors() { assert_eq!(problem.cost_bound(), &5); assert!(problem.is_weighted()); - problem.set_vertex_weights(vec![1; 6]); - problem.set_arc_costs(vec![2; 8]); + problem.set_vertex_weights(vec![1; 6]).unwrap(); + problem.set_arc_costs(vec![2; 8]).unwrap(); assert_eq!(problem.vertex_weights(), &[1, 1, 1, 1, 1, 1]); assert_eq!(problem.arc_costs(), &[2, 2, 2, 2, 2, 2, 2, 2]); } @@ -98,7 +106,14 @@ fn test_acyclic_partition_creation_and_accessors() { #[test] fn test_acyclic_partition_rejects_weight_length_mismatch() { let result = std::panic::catch_unwind(|| { - AcyclicPartition::new(DirectedGraph::new(2, vec![(0, 1)]), vec![1], vec![1], 2, 1) + AcyclicPartition::new( + DirectedGraph::new(2, vec![(0, 1)]).unwrap(), + vec![1], + vec![1], + 2, + 1, + ) + .unwrap() }); assert!(result.is_err()); } @@ -107,12 +122,13 @@ fn test_acyclic_partition_rejects_weight_length_mismatch() { fn test_acyclic_partition_rejects_arc_cost_length_mismatch() { let result = std::panic::catch_unwind(|| { AcyclicPartition::new( - DirectedGraph::new(2, vec![(0, 1)]), + DirectedGraph::new(2, vec![(0, 1)]).unwrap(), vec![1, 1], vec![], 2, 1, ) + .unwrap() }); assert!(result.is_err()); } @@ -211,7 +227,7 @@ fn test_acyclic_partition_serialization() { #[test] fn test_acyclic_partition_num_variables() { let problem = yes_instance(); - assert_eq!(problem.num_variables(), 6); + assert_eq!(problem.num_variables().unwrap(), 6); } #[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 af225e573..032ea8f6c 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,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_builds_bipartite_graph_and_rejects_invalid_edges() { @@ -44,6 +43,7 @@ fn issue_instance_1_graph() -> BipartiteGraph { (3, 1), ], ) + .unwrap() } fn issue_instance_2_graph() -> BipartiteGraph { @@ -65,6 +65,7 @@ fn issue_instance_2_graph() -> BipartiteGraph { (3, 3), ], ) + .unwrap() } fn issue_instance_2_witness() -> Vec { @@ -80,7 +81,10 @@ fn test_balanced_complete_bipartite_subgraph_creation() { assert_eq!(problem.num_vertices(), 8); assert_eq!(problem.num_edges(), 10); assert_eq!(problem.k(), 2); - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); } #[test] diff --git a/src/unit_tests/models/graph/biclique_cover.rs b/src/unit_tests/models/graph/biclique_cover.rs index ffcfb1f4a..20af7c850 100644 --- a/src/unit_tests/models/graph/biclique_cover.rs +++ b/src/unit_tests/models/graph/biclique_cover.rs @@ -62,7 +62,7 @@ fn test_biclique_cover_create_spec_rejects_out_of_bounds_edges() { assert!(matches!( invalid_left.unwrap_err(), crate::registry::ConstructionError::Conversion(message) - if message == "biedges[0] left vertex 1 is out of bounds for left partition size 1" + if message.contains("left vertex 1 out of bounds") )); let invalid_right = BicliqueCover::try_from(BicliqueCoverCreateSpec { @@ -74,18 +74,18 @@ fn test_biclique_cover_create_spec_rejects_out_of_bounds_edges() { assert!(matches!( invalid_right.unwrap_err(), crate::registry::ConstructionError::Conversion(message) - if message == "biedges[0] right vertex 1 is out of bounds for right partition size 1" + if message.contains("right vertex 1 out of bounds") )); } #[test] fn test_biclique_cover_creation() { - let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0)]); + let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0)]).unwrap(); let problem = BicliqueCover::new(graph, 2); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); assert_eq!(problem.k(), 2); - assert_eq!(problem.num_variables(), 8); // 4 vertices * 2 bicliques + assert_eq!(problem.num_variables().unwrap(), 8); // 4 vertices * 2 bicliques } #[test] @@ -95,14 +95,14 @@ fn test_from_matrix() { // [1, 0]] // Edges: (0,0), (0,1), (1,0) in local coords let matrix = vec![vec![1, 1], vec![1, 0]]; - let problem = BicliqueCover::from_matrix(&matrix, 2); + let problem = BicliqueCover::from_matrix(&matrix, 2).unwrap(); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); } #[test] fn test_get_biclique_memberships() { - let graph = BipartiteGraph::new(2, 2, vec![(0, 0)]); + let graph = BipartiteGraph::new(2, 2, vec![(0, 0)]).unwrap(); let problem = BicliqueCover::new(graph, 1); // Config: vertex 0 in biclique 0, vertex 2 in biclique 0 // Variables: [v0_b0, v1_b0, v2_b0, v3_b0] @@ -116,7 +116,7 @@ fn test_get_biclique_memberships() { #[test] fn test_is_edge_covered() { - let graph = BipartiteGraph::new(2, 2, vec![(0, 0)]); + let graph = BipartiteGraph::new(2, 2, vec![(0, 0)]).unwrap(); let problem = BicliqueCover::new(graph, 1); // Put vertex 0 and 2 in biclique 0 let config = vec![vec![true, false, true, false]]; @@ -129,7 +129,7 @@ fn test_is_edge_covered() { #[test] fn test_is_valid_cover() { - let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1)]); + let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1)]).unwrap(); let problem = BicliqueCover::new(graph, 1); // Put 0, 2, 3 in biclique 0 -> covers both edges let config = vec![vec![true, false, true, true]]; @@ -142,7 +142,7 @@ fn test_is_valid_cover() { #[test] fn test_evaluate() { - let graph = BipartiteGraph::new(2, 2, vec![(0, 0)]); + let graph = BipartiteGraph::new(2, 2, vec![(0, 0)]).unwrap(); let problem = BicliqueCover::new(graph, 1); // Valid cover with size 2 @@ -165,7 +165,7 @@ fn test_evaluate() { #[test] fn test_brute_force_simple() { // Single edge (0, 0) in local coords with k=1 - let graph = BipartiteGraph::new(2, 2, vec![(0, 0)]); + let graph = BipartiteGraph::new(2, 2, vec![(0, 0)]).unwrap(); let problem = BicliqueCover::new(graph, 1); let solver = BruteForce::new(); @@ -181,7 +181,7 @@ fn test_brute_force_simple() { fn test_brute_force_two_bicliques() { // Edges that need 2 bicliques to cover efficiently // (0,0), (1,1) in local coords - these don't share vertices - let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (1, 1)]); + let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (1, 1)]).unwrap(); let problem = BicliqueCover::new(graph, 2); let solver = BruteForce::new(); @@ -193,7 +193,7 @@ fn test_brute_force_two_bicliques() { #[test] fn test_count_covered_edges() { - let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0)]); + let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0)]).unwrap(); let problem = BicliqueCover::new(graph, 1); // Cover only (0,2): put 0 and 2 in biclique let config = vec![vec![true, false, true, false]]; @@ -225,7 +225,7 @@ fn test_is_biclique_cover_function() { #[test] fn test_empty_edges() { - let graph = BipartiteGraph::new(2, 2, vec![]); + let graph = BipartiteGraph::new(2, 2, vec![]).unwrap(); let problem = BicliqueCover::new(graph, 1); // No edges to cover -> valid with size 0 assert_eq!( @@ -239,11 +239,14 @@ fn test_biclique_problem() { 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)]); + let graph = BipartiteGraph::new(2, 2, vec![(0, 0)]).unwrap(); let problem = BicliqueCover::new(graph, 1); // dims: 4 vertices * 1 biclique = 4 binary variables - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); // Valid cover: vertex 0 and vertex 2 in biclique 0 // Config: [v0_b0=1, v1_b0=0, v2_b0=1, v3_b0=0] @@ -272,7 +275,7 @@ fn test_biclique_problem() { // ExtremumSense is minimize // Test with no edges: any config is valid - let empty_graph = BipartiteGraph::new(2, 2, vec![]); + let empty_graph = BipartiteGraph::new(2, 2, vec![]).unwrap(); let empty_problem = BicliqueCover::new(empty_graph, 1); assert_eq!( empty_problem.evaluate(&vec![vec![false; 4]]).unwrap(), @@ -284,7 +287,7 @@ fn test_biclique_problem() { fn test_is_valid_solution() { use crate::topology::BipartiteGraph; // Single edge (0,0) with 1 biclique - let graph = BipartiteGraph::new(1, 1, vec![(0, 0)]); + let graph = BipartiteGraph::new(1, 1, vec![(0, 0)]).unwrap(); let problem = BicliqueCover::new(graph, 1); // 2 vertices (left_0, right_0), 1 biclique → config length = 2 // Valid: both vertices in biclique 0 → covers edge (0,0) @@ -295,7 +298,7 @@ fn test_is_valid_solution() { #[test] fn test_parameter_getters() { - let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1)]); + let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1)]).unwrap(); let problem = BicliqueCover::new(graph, 1); assert_eq!(problem.num_vertices(), 4); // 2 left + 2 right assert_eq!(problem.num_edges(), 2); @@ -305,13 +308,18 @@ fn test_parameter_getters() { #[test] fn test_complexity_includes_number_of_bicliques() { - let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1)]); + let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1)]).unwrap(); let problem = BicliqueCover::new(graph, 2); let entry = inventory::iter::() .find(|entry| entry.name == "BicliqueCover") .expect("BicliqueCover variant should be registered"); - assert_eq!(problem.dimensions().len(), 8); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 8 + ); assert_eq!( (entry.complexity_eval_fn)(&problem as &dyn std::any::Any), 256.0 @@ -321,7 +329,7 @@ fn test_complexity_includes_number_of_bicliques() { #[test] fn test_biclique_paper_example() { // Paper: L={ℓ_1,ℓ_2}, R={r_1,r_2,r_3}, 4 edges, k=2, total size=6 - let graph = BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (1, 1), (1, 2)]); + let graph = BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (1, 1), (1, 2)]).unwrap(); let problem = BicliqueCover::new(graph, 2); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 4); diff --git a/src/unit_tests/models/graph/biconnectivity_augmentation.rs b/src/unit_tests/models/graph/biconnectivity_augmentation.rs index eae27d5eb..c03201505 100644 --- a/src/unit_tests/models/graph/biconnectivity_augmentation.rs +++ b/src/unit_tests/models/graph/biconnectivity_augmentation.rs @@ -20,7 +20,8 @@ use crate::types::One; #[test] fn test_biconnectivity_augmentation_creation() { let graph = SimpleGraph::path(4); - let problem = BiconnectivityAugmentation::new(graph.clone(), vec![(0, 3, 2), (1, 3, 1)], 2); + let problem = + BiconnectivityAugmentation::new(graph.clone(), vec![(0, 3, 2), (1, 3, 1)], 2).unwrap(); assert_eq!(problem.graph(), &graph); assert_eq!(problem.potential_weights(), &[(0, 3, 2), (1, 3, 1)]); @@ -28,8 +29,11 @@ fn test_biconnectivity_augmentation_creation() { assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); assert_eq!(problem.num_potential_edges(), 2); - assert_eq!(problem.dimensions(), vec![2, 2]); - assert_eq!(problem.num_variables(), 2); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2] + ); + assert_eq!(problem.num_variables().unwrap(), 2); assert!(problem.is_weighted()); assert_eq!( as Problem>::NAME, @@ -41,26 +45,27 @@ fn test_biconnectivity_augmentation_creation() { ); let unit_problem = - BiconnectivityAugmentation::<_, One>::new(SimpleGraph::path(3), vec![(0, 2, One)], 1); + BiconnectivityAugmentation::<_, One>::new(SimpleGraph::path(3), vec![(0, 2, One)], 1) + .unwrap(); assert!(!unit_problem.is_weighted()); } #[test] -#[should_panic(expected = "references vertex >= num_vertices")] fn test_biconnectivity_augmentation_creation_rejects_invalid_potential_edge() { - BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 4, 1)], 1); + assert!(BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 4, 1)], 1).is_err()); } #[test] -#[should_panic(expected = "already exists in the graph")] fn test_biconnectivity_augmentation_creation_rejects_existing_edge_candidate() { - BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(1, 2, 1)], 1); + assert!(BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(1, 2, 1)], 1).is_err()); } #[test] -#[should_panic(expected = "is duplicated")] fn test_biconnectivity_augmentation_creation_rejects_duplicate_candidate() { - BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 3, 1), (3, 0, 2)], 2); + assert!( + BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 3, 1), (3, 0, 2)], 2) + .is_err() + ); } #[test] @@ -69,7 +74,8 @@ fn test_biconnectivity_augmentation_evaluation() { SimpleGraph::path(4), vec![(0, 2, 5), (1, 3, 1), (0, 3, 2)], 2, - ); + ) + .unwrap(); assert!(!problem.evaluate(&vec![false, false, false]).unwrap()); assert!(!problem.evaluate(&vec![false, true, false]).unwrap()); @@ -89,7 +95,8 @@ fn test_biconnectivity_augmentation_evaluation() { #[test] fn test_biconnectivity_augmentation_serialization() { let problem = - BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 3, 2), (1, 3, 1)], 2); + BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 3, 2), (1, 3, 1)], 2) + .unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: BiconnectivityAugmentation = @@ -106,7 +113,8 @@ fn test_biconnectivity_augmentation_solver() { SimpleGraph::path(4), vec![(0, 2, 5), (1, 3, 1), (0, 3, 2)], 2, - ); + ) + .unwrap(); let solver = BruteForce::new(); let solution = solver @@ -121,7 +129,8 @@ fn test_biconnectivity_augmentation_solver() { #[test] fn test_biconnectivity_augmentation_no_solution() { - let problem = BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 2, 1)], 1); + let problem = + BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 2, 1)], 1).unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); @@ -152,31 +161,47 @@ fn test_biconnectivity_augmentation_paper_example() { (3, 5, 1), ], 3, - ); + ) + .unwrap(); assert!(!over_budget_problem.evaluate(&satisfying_config).unwrap()); assert!(solver.solve(&over_budget_problem).unwrap().is_none()); } #[test] fn test_is_biconnected() { - assert!(is_biconnected(&SimpleGraph::cycle(4))); - assert!(is_biconnected(&SimpleGraph::complete(3))); - assert!(!is_biconnected(&SimpleGraph::path(4))); - assert!(!is_biconnected(&SimpleGraph::new(4, vec![(0, 1), (2, 3)]))); + for (graph, expected) in [ + (SimpleGraph::empty(0), true), + (SimpleGraph::empty(1), true), + (SimpleGraph::empty(2), false), + (SimpleGraph::path(2), true), + (SimpleGraph::cycle(4), true), + (SimpleGraph::complete(3), true), + (SimpleGraph::path(4), false), + (SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(), false), + ( + SimpleGraph::new(2, vec![(0, 0), (0, 1), (0, 1)]).unwrap(), + true, + ), + ] { + let problem = BiconnectivityAugmentation::<_, i64>::new(graph, vec![], 0).unwrap(); + assert_eq!(problem.evaluate(&vec![]).unwrap().0, expected); + } } #[test] fn test_biconnectivity_augmentation_signed_total_budget() { for candidates in [vec![(0, 2, 2), (0, 3, -2)], vec![(0, 3, -2), (0, 2, 2)]] { - let source = BiconnectivityAugmentation::new(SimpleGraph::path(4), candidates, 0); + let source = BiconnectivityAugmentation::new(SimpleGraph::path(4), candidates, 0).unwrap(); assert!(source.evaluate(&vec![true, true]).unwrap().0); } for n in 0..=3 { let source = - BiconnectivityAugmentation::<_, i64>::new(SimpleGraph::complete(n), vec![], -1); + BiconnectivityAugmentation::<_, i64>::new(SimpleGraph::complete(n), vec![], -1) + .unwrap(); assert!(!source.evaluate(&vec![]).unwrap().0); } - let source = BiconnectivityAugmentation::new(SimpleGraph::path(3), vec![(0, 2, -3)], -2); + let source = + BiconnectivityAugmentation::new(SimpleGraph::path(3), vec![(0, 2, -3)], -2).unwrap(); assert!(source.evaluate(&vec![true]).unwrap().0); } @@ -186,7 +211,8 @@ fn test_biconnectivity_augmentation_preserves_checked_arithmetic() { SimpleGraph::path(4), vec![(0, 2, i64::MAX), (0, 3, 1), (1, 3, -1)], i64::MAX, - ); + ) + .unwrap(); assert!(matches!( source.evaluate(&vec![true, true, true]), Err(crate::traits::EvaluationError::IntegerOverflow(_)) @@ -195,7 +221,8 @@ fn test_biconnectivity_augmentation_preserves_checked_arithmetic() { SimpleGraph::path(4), vec![(0, 2, i64::MIN), (0, 3, -1)], i64::MAX, - ); + ) + .unwrap(); assert!(matches!( source.evaluate(&vec![true, true]), Err(crate::traits::EvaluationError::IntegerOverflow(_)) diff --git a/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs b/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs index 9aa70a24d..9da2cbc72 100644 --- a/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs +++ b/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs @@ -21,9 +21,11 @@ fn k5_btsp() -> BottleneckTravelingSalesman { (2, 4), (3, 4), ], - ), + ) + .unwrap(), vec![5, 4, 4, 5, 4, 1, 2, 1, 5, 4], ) + .unwrap() } #[test] @@ -34,8 +36,11 @@ fn test_bottleneck_traveling_salesman_creation_and_parameter_getters() { assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.graph().num_edges(), 10); assert_eq!(problem.num_edges(), 10); - assert_eq!(problem.dimensions(), vec![2; 10]); - assert_eq!(problem.num_variables(), 10); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 10] + ); + assert_eq!(problem.num_variables().unwrap(), 10); assert_eq!(problem.weights(), vec![5, 4, 4, 5, 4, 1, 2, 1, 5, 4]); assert_eq!( problem.edges(), @@ -54,7 +59,9 @@ fn test_bottleneck_traveling_salesman_creation_and_parameter_getters() { ); assert!(problem.is_weighted()); - problem.set_weights(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + problem + .set_weights(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + .unwrap(); assert_eq!(problem.weights(), vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); } @@ -78,9 +85,10 @@ fn test_bottleneck_traveling_salesman_evaluate_valid_and_invalid() { #[test] fn test_bottleneck_traveling_salesman_evaluate_disconnected_subtour_invalid() { let problem = BottleneckTravelingSalesman::new( - SimpleGraph::new(6, vec![(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5)]), + SimpleGraph::new(6, vec![(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5)]).unwrap(), vec![1, 1, 1, 2, 2, 2], - ); + ) + .unwrap(); let disconnected_subtour = vec![true, true, true, true, true, true]; assert!(!problem.is_valid_solution(&disconnected_subtour)); 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 020d1bfbb..7d00c1d5c 100644 --- a/src/unit_tests/models/graph/bounded_component_spanning_forest.rs +++ b/src/unit_tests/models/graph/bounded_component_spanning_forest.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use std::alloc::{GlobalAlloc, Layout, System}; @@ -16,7 +15,7 @@ fn create_spec_uses_k_and_max_weight_inputs() { assert_eq!(names, ["graph", "weights", "k", "max_weight"]); let problem = BoundedComponentSpanningForest::try_from(BoundedComponentSpanningForestCreateSpec { - graph: SimpleGraph::new(2, vec![(0, 1)]), + graph: SimpleGraph::new(2, vec![(0, 1)]).unwrap(), weights: vec![1, 2], k: 1, max_weight: 3, @@ -85,13 +84,14 @@ fn yes_instance() -> BoundedComponentSpanningForest { (1, 5), (2, 6), ], - ); - BoundedComponentSpanningForest::new(graph, vec![2, 3, 1, 2, 3, 1, 2, 1], 3, 6) + ) + .unwrap(); + BoundedComponentSpanningForest::new(graph, vec![2, 3, 1, 2, 3, 1, 2, 1], 3, 6).unwrap() } fn no_instance() -> BoundedComponentSpanningForest { - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]); - BoundedComponentSpanningForest::new(graph, vec![1, 1, 1, 1, 1, 1], 2, 2) + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]).unwrap(); + BoundedComponentSpanningForest::new(graph, vec![1, 1, 1, 1, 1, 1], 2, 2).unwrap() } #[test] @@ -104,7 +104,10 @@ fn test_bounded_component_spanning_forest_creation() { assert_eq!(problem.max_weight(), &6); assert_eq!(problem.num_vertices(), 8); assert_eq!(problem.num_edges(), 10); - assert_eq!(problem.dimensions(), vec![3; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 8] + ); assert!(problem.is_weighted()); } @@ -146,7 +149,8 @@ fn test_bounded_component_spanning_forest_rejects_wrong_length() { #[test] fn test_bounded_component_spanning_forest_evaluate_uses_fixed_allocation_budget() { - let problem = BoundedComponentSpanningForest::new(SimpleGraph::empty(16), vec![1; 16], 16, 1); + let problem = + BoundedComponentSpanningForest::new(SimpleGraph::empty(16), vec![1; 16], 16, 1).unwrap(); let config: Vec = (0..16).collect(); let (is_valid, allocations) = count_allocations(|| problem.evaluate(&config).unwrap()); @@ -195,31 +199,28 @@ fn test_bounded_component_spanning_forest_paper_example() { } #[test] -#[should_panic(expected = "max_components must be at least 1")] fn test_bounded_component_spanning_forest_rejects_zero_max_components_in_constructor() { - let graph = SimpleGraph::new(2, vec![(0, 1)]); - let _ = BoundedComponentSpanningForest::new(graph, vec![1, 1], 0, 1); + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); + assert!(BoundedComponentSpanningForest::new(graph, vec![1, 1], 0, 1).is_err()); } #[test] fn test_bounded_component_spanning_forest_accepts_k_larger_than_num_vertices() { - let graph = SimpleGraph::new(2, vec![(0, 1)]); - let problem = BoundedComponentSpanningForest::new(graph, vec![1, 1], 5, 2); + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); + let problem = BoundedComponentSpanningForest::new(graph, vec![1, 1], 5, 2).unwrap(); // K > |V| is mathematically harmless — just means fewer than K components possible assert_eq!(problem.max_components(), 5); assert!(problem.evaluate(&vec![0, 0]).unwrap()); } #[test] -#[should_panic(expected = "weights must be nonnegative")] fn test_bounded_component_spanning_forest_rejects_negative_weights_in_constructor() { - let graph = SimpleGraph::new(2, vec![(0, 1)]); - let _ = BoundedComponentSpanningForest::new(graph, vec![1, -1], 1, 1); + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); + assert!(BoundedComponentSpanningForest::new(graph, vec![1, -1], 1, 1).is_err()); } #[test] -#[should_panic(expected = "max_weight must be positive")] fn test_bounded_component_spanning_forest_rejects_nonpositive_bound_in_constructor() { - let graph = SimpleGraph::new(2, vec![(0, 1)]); - let _ = BoundedComponentSpanningForest::new(graph, vec![1, 1], 1, 0); + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); + assert!(BoundedComponentSpanningForest::new(graph, vec![1, 1], 1, 0).is_err()); } diff --git a/src/unit_tests/models/graph/bounded_diameter_spanning_tree.rs b/src/unit_tests/models/graph/bounded_diameter_spanning_tree.rs index 77c13c30f..b0a06efe7 100644 --- a/src/unit_tests/models/graph/bounded_diameter_spanning_tree.rs +++ b/src/unit_tests/models/graph/bounded_diameter_spanning_tree.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -12,11 +11,13 @@ fn example_instance() -> BoundedDiameterSpanningTree { SimpleGraph::new( 5, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 4), (2, 3), (3, 4)], - ), + ) + .unwrap(), vec![1, 2, 1, 1, 2, 1, 1], 5, 3, ) + .unwrap() } #[test] @@ -26,7 +27,10 @@ fn test_bounded_diameter_spanning_tree_creation() { assert_eq!(problem.num_edges(), 7); assert_eq!(problem.weight_bound(), &5); assert_eq!(problem.diameter_bound(), 3); - assert_eq!(problem.dimensions(), vec![2; 7]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 7] + ); assert_eq!(problem.graph().num_vertices(), 5); assert_eq!(problem.edge_list().len(), 7); assert_eq!(problem.edge_weights().len(), 7); @@ -60,11 +64,12 @@ fn test_bounded_diameter_spanning_tree_evaluate_exceeds_weight() { fn test_bounded_diameter_spanning_tree_evaluate_exceeds_diameter() { // Create instance with very tight diameter bound let problem = BoundedDiameterSpanningTree::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1, 1, 1], 10, 1, // diameter ≤ 1 means all vertices must be distance 1 from each other - ); + ) + .unwrap(); // The only spanning tree is the path 0-1-2-3 with diameter 3 assert!(!problem.evaluate(&vec![true, true, true]).unwrap()); } @@ -109,11 +114,12 @@ fn test_bounded_diameter_spanning_tree_infeasible() { // Path graph 0-1-2-3-4, all weight 1, weight bound 10 but diameter bound 2 // Only spanning tree is the path itself with diameter 4 > 2 let problem = BoundedDiameterSpanningTree::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(), vec![1, 1, 1, 1], 10, 2, - ); + ) + .unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); } @@ -131,21 +137,25 @@ fn test_bounded_diameter_spanning_tree_serialization() { } #[test] -#[should_panic(expected = "diameter_bound must be at least 1")] -fn test_bounded_diameter_spanning_tree_zero_diameter_panics() { - let _ = BoundedDiameterSpanningTree::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), +fn test_bounded_diameter_spanning_tree_zero_diameter_rejects() { + assert!(BoundedDiameterSpanningTree::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1, 1], 5, 0, - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "edge_weights length must match num_edges")] -fn test_bounded_diameter_spanning_tree_wrong_weights_length_panics() { - let _ = - BoundedDiameterSpanningTree::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1], 5, 2); +fn test_bounded_diameter_spanning_tree_wrong_weights_length_rejects() { + assert!(BoundedDiameterSpanningTree::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1], + 5, + 2 + ) + .is_err()); } #[test] fn create_spec_uses_edge_weights_and_defaults_to_one() { diff --git a/src/unit_tests/models/graph/degree_constrained_spanning_tree.rs b/src/unit_tests/models/graph/degree_constrained_spanning_tree.rs index 847436d94..015d0257c 100644 --- a/src/unit_tests/models/graph/degree_constrained_spanning_tree.rs +++ b/src/unit_tests/models/graph/degree_constrained_spanning_tree.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -10,9 +9,11 @@ fn example_instance() -> DegreeConstrainedSpanningTree { SimpleGraph::new( 5, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 4), (2, 3), (3, 4)], - ), + ) + .unwrap(), 2, ) + .unwrap() } #[test] @@ -21,7 +22,10 @@ fn test_degree_constrained_spanning_tree_creation() { assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 7); assert_eq!(problem.max_degree(), 2); - assert_eq!(problem.dimensions(), vec![2; 7]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 7] + ); assert_eq!(problem.graph().num_vertices(), 5); assert_eq!(problem.edge_list().len(), 7); } @@ -104,9 +108,10 @@ fn test_degree_constrained_spanning_tree_infeasible() { // Only spanning tree is the star itself, which has degree 4 at vertex 0. // With K=2, no spanning tree exists. let problem = DegreeConstrainedSpanningTree::new( - SimpleGraph::new(5, vec![(0, 1), (0, 2), (0, 3), (0, 4)]), + SimpleGraph::new(5, vec![(0, 1), (0, 2), (0, 3), (0, 4)]).unwrap(), 2, - ); + ) + .unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); } @@ -115,13 +120,17 @@ fn test_degree_constrained_spanning_tree_infeasible() { fn test_degree_constrained_spanning_tree_k1_path() { // K=1 means the tree is a single edge for n=2. // For n>2, K=1 is impossible since a tree on n>=3 vertices must have max degree >= 2. - let problem = - DegreeConstrainedSpanningTree::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), 1); + let problem = DegreeConstrainedSpanningTree::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), + 1, + ) + .unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); // For n=2, K=1 works: the single edge is the tree. - let problem2 = DegreeConstrainedSpanningTree::new(SimpleGraph::new(2, vec![(0, 1)]), 1); + let problem2 = + DegreeConstrainedSpanningTree::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 1).unwrap(); let solver2 = BruteForce::new(); let sol = solver2.solve(&problem2).unwrap(); assert!(sol.is_some()); @@ -139,7 +148,10 @@ fn test_degree_constrained_spanning_tree_serialization() { } #[test] -#[should_panic(expected = "max_degree must be at least 1")] -fn test_degree_constrained_spanning_tree_zero_k_panics() { - let _ = DegreeConstrainedSpanningTree::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 0); +fn test_degree_constrained_spanning_tree_zero_k_rejects() { + assert!(DegreeConstrainedSpanningTree::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + 0 + ) + .is_err()); } diff --git a/src/unit_tests/models/graph/directed_hamiltonian_path.rs b/src/unit_tests/models/graph/directed_hamiltonian_path.rs index d00fa1c5f..2975350b4 100644 --- a/src/unit_tests/models/graph/directed_hamiltonian_path.rs +++ b/src/unit_tests/models/graph/directed_hamiltonian_path.rs @@ -1,24 +1,26 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::DirectedGraph; use crate::traits::Problem; #[test] fn test_directed_hamiltonian_path_creation() { // Simple directed path: 0->1->2->3 - let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = DirectedHamiltonianPath::new(graph); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_arcs(), 3); // Lehmer dims: [4, 3, 2, 1] - assert_eq!(problem.dimensions(), vec![4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 3, 2, 1] + ); } #[test] fn test_directed_hamiltonian_path_evaluate_valid() { // Directed path: 0->1->2->3 - let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = DirectedHamiltonianPath::new(graph); assert_eq!( @@ -35,7 +37,7 @@ fn test_directed_hamiltonian_path_evaluate_valid() { #[test] fn test_directed_hamiltonian_path_evaluate_invalid_no_arc() { // Only arc 0->1 and 2->3, not 1->2 - let graph = DirectedGraph::new(4, vec![(0, 1), (2, 3)]); + let graph = DirectedGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(); let problem = DirectedHamiltonianPath::new(graph); // No Hamiltonian path should be valid let solver = BruteForce::new(); @@ -45,7 +47,7 @@ fn test_directed_hamiltonian_path_evaluate_invalid_no_arc() { #[test] fn test_directed_hamiltonian_path_brute_force() { // Simple directed path graph - let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = DirectedHamiltonianPath::new(graph); let solver = BruteForce::new(); let solution = solver @@ -74,7 +76,8 @@ fn test_directed_hamiltonian_path_issue_example() { (4, 5), (5, 1), ], - ); + ) + .unwrap(); let problem = DirectedHamiltonianPath::new(graph); let path = vec![0usize, 1, 3, 2, 4, 5]; assert_eq!( @@ -87,7 +90,7 @@ fn test_directed_hamiltonian_path_issue_example() { #[test] fn test_directed_hamiltonian_path_no_solution() { // Directed graph with no Hamiltonian path: 0->1, 0->2, no outgoing from 1 or 2 - let graph = DirectedGraph::new(3, vec![(0, 1), (0, 2)]); + let graph = DirectedGraph::new(3, vec![(0, 1), (0, 2)]).unwrap(); let problem = DirectedHamiltonianPath::new(graph); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); @@ -95,7 +98,7 @@ fn test_directed_hamiltonian_path_no_solution() { #[test] fn test_directed_hamiltonian_path_single_vertex() { - let graph = DirectedGraph::new(1, vec![]); + let graph = DirectedGraph::new(1, vec![]).unwrap(); let problem = DirectedHamiltonianPath::new(graph); // Single vertex: trivially Hamiltonian assert_eq!(problem.evaluate(&vec![0]).unwrap(), crate::types::Or(true)); @@ -106,7 +109,7 @@ fn test_directed_hamiltonian_path_single_vertex() { #[test] fn test_directed_hamiltonian_path_serialization() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = DirectedHamiltonianPath::new(graph); let json = serde_json::to_value(&problem).unwrap(); let deserialized: DirectedHamiltonianPath = serde_json::from_value(json).unwrap(); @@ -116,7 +119,7 @@ fn test_directed_hamiltonian_path_serialization() { #[test] fn test_is_valid_solution() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = DirectedHamiltonianPath::new(graph); // Valid: path [0, 1, 2] assert!(problem.is_valid_solution(&[0, 1, 2])); @@ -126,12 +129,15 @@ fn test_is_valid_solution() { #[test] fn test_parameter_getters() { - let graph = DirectedGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); + let graph = DirectedGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(); let problem = DirectedHamiltonianPath::new(graph); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_arcs(), 4); // Lehmer dims: [5, 4, 3, 2, 1] - assert_eq!(problem.dimensions(), vec![5, 4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 4, 3, 2, 1] + ); } #[test] 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 444a2e69f..254906f39 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,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -19,16 +18,17 @@ fn yes_instance() -> DirectedTwoCommodityIntegralFlow { (3, 4), (3, 5), ], - ); - DirectedTwoCommodityIntegralFlow::new(graph, vec![1; 8], 0, 4, 1, 5, 1, 1) + ) + .unwrap(); + DirectedTwoCommodityIntegralFlow::new(graph, vec![1; 8], 0, 4, 1, 5, 1, 1).unwrap() } /// NO instance: 4 vertices, 3 arcs (all capacity 1). /// s1=0, t1=3, s2=1, t2=3, R1=1, R2=1. /// Bottleneck at arc (2,3) with capacity 1. fn no_instance() -> DirectedTwoCommodityIntegralFlow { - let graph = DirectedGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]); - DirectedTwoCommodityIntegralFlow::new(graph, vec![1; 3], 0, 3, 1, 3, 1, 1) + let graph = DirectedGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]).unwrap(); + DirectedTwoCommodityIntegralFlow::new(graph, vec![1; 3], 0, 3, 1, 3, 1, 1).unwrap() } #[test] @@ -36,8 +36,16 @@ fn test_directed_two_commodity_integral_flow_creation() { let problem = yes_instance(); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_arcs(), 8); - assert_eq!(problem.dimensions().len(), 16); // 2 * 8 - assert!(problem.dimensions().iter().all(|&d| d == 2)); // capacity 1 -> domain {0,1} + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 16 + ); // 2 * 8 + assert!(crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .iter() + .all(|&d| d == 2)); // capacity 1 -> domain {0,1} assert_eq!(problem.source_1(), 0); assert_eq!(problem.sink_1(), 4); assert_eq!(problem.source_2(), 1); @@ -88,8 +96,8 @@ fn test_directed_two_commodity_integral_flow_conservation_violation() { #[test] fn test_directed_two_commodity_integral_flow_negative_net_flow_at_sink_is_infeasible() { - let graph = DirectedGraph::new(3, vec![(1, 2)]); - let problem = DirectedTwoCommodityIntegralFlow::new(graph, vec![1], 0, 1, 2, 2, 1, 0); + let graph = DirectedGraph::new(3, vec![(1, 2)]).unwrap(); + let problem = DirectedTwoCommodityIntegralFlow::new(graph, vec![1], 0, 1, 2, 2, 1, 0).unwrap(); // Commodity 1 sends flow out of its sink with no incoming flow. let config = vec![1, 0]; @@ -98,8 +106,9 @@ fn test_directed_two_commodity_integral_flow_negative_net_flow_at_sink_is_infeas #[test] fn test_directed_two_commodity_integral_flow_disallows_using_other_commodity_source() { - let graph = DirectedGraph::new(4, vec![(2, 3), (3, 1)]); - let problem = DirectedTwoCommodityIntegralFlow::new(graph, vec![1, 1], 0, 1, 2, 3, 1, 0); + let graph = DirectedGraph::new(4, vec![(2, 3), (3, 1)]).unwrap(); + let problem = + DirectedTwoCommodityIntegralFlow::new(graph, vec![1, 1], 0, 1, 2, 3, 1, 0).unwrap(); // Commodity 1 reaches t1 from s2, which is illegal in the classical definition: // conservation must hold for commodity 1 at s2. @@ -196,7 +205,7 @@ fn test_directed_two_commodity_integral_flow_wrong_config_length() { #[test] fn test_directed_two_commodity_integral_flow_higher_capacity() { // Test with capacity 2: two paths can share an arc - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = DirectedTwoCommodityIntegralFlow::new( graph, vec![2, 2], // capacity 2 on both arcs @@ -206,8 +215,12 @@ fn test_directed_two_commodity_integral_flow_higher_capacity() { 2, 1, 1, - ); - assert_eq!(problem.dimensions(), vec![3, 3, 3, 3]); // each variable in {0,1,2} + ) + .unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3, 3, 3] + ); // each variable in {0,1,2} // Both commodities can share: f1=1, f2=1 on both arcs let config = vec![1, 1, 1, 1]; diff --git a/src/unit_tests/models/graph/disjoint_connecting_paths.rs b/src/unit_tests/models/graph/disjoint_connecting_paths.rs index c1e93c582..e978627ab 100644 --- a/src/unit_tests/models/graph/disjoint_connecting_paths.rs +++ b/src/unit_tests/models/graph/disjoint_connecting_paths.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_reused_terminal() { assert!( @@ -20,9 +19,11 @@ fn issue_yes_problem() -> DisjointConnectingPaths { SimpleGraph::new( 6, vec![(0, 1), (1, 3), (0, 2), (1, 4), (2, 4), (3, 5), (4, 5)], - ), + ) + .unwrap(), vec![(0, 3), (2, 5)], ) + .unwrap() } fn issue_yes_config() -> Vec { @@ -31,9 +32,10 @@ fn issue_yes_config() -> Vec { fn issue_no_problem() -> DisjointConnectingPaths { DisjointConnectingPaths::new( - SimpleGraph::new(6, vec![(0, 2), (1, 2), (2, 3), (3, 4), (3, 5)]), + SimpleGraph::new(6, vec![(0, 2), (1, 2), (2, 3), (3, 4), (3, 5)]).unwrap(), vec![(0, 4), (1, 5)], ) + .unwrap() } #[test] @@ -43,7 +45,10 @@ fn test_disjoint_connecting_paths_creation() { assert_eq!(problem.num_edges(), 7); assert_eq!(problem.num_pairs(), 2); assert_eq!(problem.terminal_pairs(), &[(0, 3), (2, 5)]); - assert_eq!(problem.dimensions(), vec![2; 7]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 7] + ); assert_eq!( problem.ordered_edges(), vec![(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (3, 5), (4, 5)] @@ -51,18 +56,19 @@ fn test_disjoint_connecting_paths_creation() { } #[test] -#[should_panic(expected = "terminal_pairs must contain at least one pair")] fn test_disjoint_connecting_paths_rejects_empty_pairs() { - let _ = DisjointConnectingPaths::new(SimpleGraph::new(2, vec![(0, 1)]), vec![]); + assert!( + DisjointConnectingPaths::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![]).is_err() + ); } #[test] -#[should_panic(expected = "terminal vertices must be pairwise disjoint across pairs")] fn test_disjoint_connecting_paths_rejects_overlapping_terminals() { - let _ = DisjointConnectingPaths::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + assert!(DisjointConnectingPaths::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![(0, 2), (2, 3)], - ); + ) + .is_err()); } #[test] diff --git a/src/unit_tests/models/graph/eulerian_path.rs b/src/unit_tests/models/graph/eulerian_path.rs index 2b6724a77..8cb15279b 100644 --- a/src/unit_tests/models/graph/eulerian_path.rs +++ b/src/unit_tests/models/graph/eulerian_path.rs @@ -11,7 +11,7 @@ use crate::types::Or; /// arcs `a_0` and `a_1` between vertices `0` and `1`. The witness ordering /// `(a_0, a_2, a_3, a_1)` traces the directed trail `0 -> 1 -> 2 -> 0 -> 1`. fn canonical_instance() -> EulerianPath { - let graph = DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)]); + let graph = DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)]).unwrap(); EulerianPath::new(graph) } @@ -21,8 +21,11 @@ fn test_eulerian_path_creation() { assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_arcs(), 4); // m = 4 position variables, each with domain {0..3}. - assert_eq!(problem.dimensions(), vec![4, 4, 4, 4]); - assert_eq!(problem.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 4, 4, 4] + ); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] @@ -90,7 +93,7 @@ fn test_eulerian_path_no_instance() { // Three parallel arcs (0,1) and one return arc (1,0). // outdeg(0) - indeg(0) = 3 - 1 = 2, breaks the degree-balance condition, // so no Eulerian trail exists. - let graph = DirectedGraph::new(2, vec![(0, 1), (0, 1), (0, 1), (1, 0)]); + let graph = DirectedGraph::new(2, vec![(0, 1), (0, 1), (0, 1), (1, 0)]).unwrap(); let problem = EulerianPath::new(graph); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); @@ -101,10 +104,13 @@ fn test_eulerian_path_no_instance() { #[test] fn test_eulerian_path_empty_arcs_instance() { // m = 0 (only isolated vertices): dims = [] and the empty witness is valid. - let graph = DirectedGraph::new(3, vec![]); + let graph = DirectedGraph::new(3, vec![]).unwrap(); let problem = EulerianPath::new(graph); - assert_eq!(problem.dimensions(), Vec::::new()); - assert_eq!(problem.num_variables(), 0); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); + assert_eq!(problem.num_variables().unwrap(), 0); assert_eq!(problem.evaluate(&vec![]).unwrap(), Or(true)); let solver = BruteForce::new(); diff --git a/src/unit_tests/models/graph/generalized_hex.rs b/src/unit_tests/models/graph/generalized_hex.rs index dad6efcb1..491809daf 100644 --- a/src/unit_tests/models/graph/generalized_hex.rs +++ b/src/unit_tests/models/graph/generalized_hex.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -8,7 +7,7 @@ use crate::traits::Problem; fn create_spec_uses_sink_input() { assert_eq!(GeneralizedHexCreateSpec::FIELDS[2].name, "sink"); let problem = GeneralizedHex::try_from(GeneralizedHexCreateSpec { - graph: SimpleGraph::new(2, vec![(0, 1)]), + graph: SimpleGraph::new(2, vec![(0, 1)]).unwrap(), source: 0, sink: 1, }) @@ -32,10 +31,12 @@ fn issue_example() -> GeneralizedHex { (5, 6), (6, 7), ], - ), + ) + .unwrap(), 0, 7, ) + .unwrap() } fn winning_example() -> GeneralizedHex { @@ -43,10 +44,12 @@ fn winning_example() -> GeneralizedHex { SimpleGraph::new( 6, vec![(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4), (4, 5)], - ), + ) + .unwrap(), 0, 5, ) + .unwrap() } #[test] @@ -57,7 +60,10 @@ fn test_generalized_hex_creation_and_getters() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 7); assert_eq!(problem.num_playable_vertices(), 4); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.graph().num_vertices(), 6); } @@ -69,7 +75,12 @@ fn test_generalized_hex_forced_win_on_bottleneck_example() { #[test] fn test_generalized_hex_detects_losing_position() { - let problem = GeneralizedHex::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), 0, 3); + let problem = GeneralizedHex::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), + 0, + 3, + ) + .unwrap(); assert!(!problem.evaluate(&()).unwrap()); } @@ -113,7 +124,6 @@ fn test_generalized_hex_paper_example() { } #[test] -#[should_panic(expected = "source and target must be distinct")] fn test_generalized_hex_rejects_identical_terminals() { - let _ = GeneralizedHex::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 1, 1); + assert!(GeneralizedHex::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 1, 1).is_err()); } diff --git a/src/unit_tests/models/graph/graph_partitioning.rs b/src/unit_tests/models/graph/graph_partitioning.rs index e29077c1a..7b6fc1c85 100644 --- a/src/unit_tests/models/graph/graph_partitioning.rs +++ b/src/unit_tests/models/graph/graph_partitioning.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -20,7 +19,8 @@ fn issue_example() -> GraphPartitioning { (3, 5), (4, 5), ], - ); + ) + .unwrap(); GraphPartitioning::new(graph) } @@ -29,7 +29,10 @@ fn test_graphpartitioning_basic() { let problem = issue_example(); // Check dims: 6 binary variables - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2, 2, 2] + ); // Evaluate a valid balanced partition: A={0,1,2}, B={3,4,5} // config: [0, 0, 0, 1, 1, 1] @@ -74,7 +77,7 @@ fn test_graphpartitioning_solver() { #[test] fn test_graphpartitioning_odd_vertices() { // 3 vertices: all configs must be Invalid since n is odd - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = GraphPartitioning::new(graph); // Every possible config should be Invalid @@ -97,7 +100,7 @@ fn test_graphpartitioning_odd_vertices() { #[test] fn test_graphpartitioning_unbalanced_invalid() { // 4 vertices: only configs with exactly 2 ones are valid - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]).unwrap(); let problem = GraphPartitioning::new(graph); // All zeros: 0 ones, not balanced @@ -153,7 +156,7 @@ fn test_graphpartitioning_parameter_getters() { #[test] fn test_graphpartitioning_square_graph() { // Square graph: 0-1, 1-2, 2-3, 3-0 (the doctest example) - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(); let problem = GraphPartitioning::new(graph); let solver = BruteForce::new(); @@ -184,7 +187,7 @@ fn test_graphpartitioning_graph_accessor() { #[test] fn test_graphpartitioning_empty_graph() { // 4 vertices, no edges: any balanced partition has cut = 0 - let graph = SimpleGraph::new(4, vec![]); + let graph = SimpleGraph::new(4, vec![]).unwrap(); let problem = GraphPartitioning::new(graph); let config = vec![false, false, true, true]; diff --git a/src/unit_tests/models/graph/hamiltonian_circuit.rs b/src/unit_tests/models/graph/hamiltonian_circuit.rs index fc2d684cb..998272fcb 100644 --- a/src/unit_tests/models/graph/hamiltonian_circuit.rs +++ b/src/unit_tests/models/graph/hamiltonian_circuit.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -21,12 +20,16 @@ fn test_hamiltonian_circuit_basic() { (1, 4), (2, 5), ], - ); + ) + .unwrap(); let problem = HamiltonianCircuit::new(graph); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 9); - assert_eq!(problem.dimensions(), vec![6; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6; 6] + ); // Valid Hamiltonian circuit: 0->1->2->5->4->3->0 // Edges used: (0,1), (1,2), (2,5), (5,4), (4,3), (3,0) -- all present @@ -54,22 +57,22 @@ fn test_hamiltonian_circuit_basic() { #[test] fn test_hamiltonian_circuit_small_graphs() { // Empty graph (0 vertices): n < 3, no circuit possible - let graph = SimpleGraph::new(0, vec![]); + let graph = SimpleGraph::new(0, vec![]).unwrap(); let problem = HamiltonianCircuit::new(graph); assert!(!problem.evaluate(&vec![]).unwrap()); // Single vertex: n < 3 - let graph = SimpleGraph::new(1, vec![]); + let graph = SimpleGraph::new(1, vec![]).unwrap(); let problem = HamiltonianCircuit::new(graph); assert!(!problem.evaluate(&vec![0]).unwrap()); // Two vertices with edge: n < 3 - let graph = SimpleGraph::new(2, vec![(0, 1)]); + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); let problem = HamiltonianCircuit::new(graph); assert!(!problem.evaluate(&vec![0, 1]).unwrap()); // Triangle (K3): smallest valid Hamiltonian circuit - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); let problem = HamiltonianCircuit::new(graph); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -80,7 +83,7 @@ fn test_hamiltonian_circuit_small_graphs() { #[test] fn test_hamiltonian_circuit_complete_graph_k4() { // K4: complete graph on 4 vertices - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); let problem = HamiltonianCircuit::new(graph); let solver = BruteForce::new(); @@ -96,7 +99,7 @@ fn test_hamiltonian_circuit_complete_graph_k4() { #[test] fn test_hamiltonian_circuit_no_solution() { // Path graph on 4 vertices: no Hamiltonian circuit possible - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = HamiltonianCircuit::new(graph); let solver = BruteForce::new(); @@ -107,7 +110,7 @@ fn test_hamiltonian_circuit_no_solution() { #[test] fn test_hamiltonian_circuit_solver() { // Cycle on 4 vertices (square): edges {0,1}, {1,2}, {2,3}, {3,0} - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(); let problem = HamiltonianCircuit::new(graph); let solver = BruteForce::new(); @@ -123,13 +126,16 @@ fn test_hamiltonian_circuit_solver() { #[test] fn test_hamiltonian_circuit_serialization() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(); let problem = HamiltonianCircuit::new(graph); let json = serde_json::to_string(&problem).unwrap(); let restored: HamiltonianCircuit = serde_json::from_str(&json).unwrap(); - assert_eq!(problem.dimensions(), restored.dimensions()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + crate::solvers::cartesian_dimensions(&restored).unwrap() + ); // Valid circuit gives the same result on both instances assert_eq!( diff --git a/src/unit_tests/models/graph/hamiltonian_path.rs b/src/unit_tests/models/graph/hamiltonian_path.rs index c86819a08..df6e90a95 100644 --- a/src/unit_tests/models/graph/hamiltonian_path.rs +++ b/src/unit_tests/models/graph/hamiltonian_path.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; #[test] @@ -8,10 +7,13 @@ fn test_hamiltonian_path_basic() { use crate::traits::Problem; // Path graph: 0-1-2-3 (has Hamiltonian path) - let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); - assert_eq!(problem.dimensions(), vec![4, 4, 4, 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 4, 4, 4] + ); // Valid path: 0->1->2->3 assert!(problem.evaluate(&vec![0, 1, 2, 3]).unwrap()); @@ -26,10 +28,9 @@ fn test_hamiltonian_path_basic() { #[test] fn test_hamiltonian_path_no_solution() { // K4 on {0,1,2,3} + two isolated vertices {4,5} - let problem = HamiltonianPath::new(SimpleGraph::new( - 6, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )); + let problem = HamiltonianPath::new( + SimpleGraph::new(6, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), + ); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); assert!( @@ -43,7 +44,7 @@ fn test_hamiltonian_path_brute_force() { use crate::traits::Problem; // Path graph P4: 0-1-2-3 - let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); @@ -63,19 +64,22 @@ fn test_hamiltonian_path_nontrivial() { use crate::traits::Problem; // Instance 2 from issue: 6 vertices, 8 edges - let problem = HamiltonianPath::new(SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (1, 3), - (2, 3), - (3, 4), - (3, 5), - (4, 2), - (5, 1), - ], - )); + let problem = HamiltonianPath::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 3), + (2, 3), + (3, 4), + (3, 5), + (4, 2), + (5, 1), + ], + ) + .unwrap(), + ); // Hamiltonian path: 0->2->4->3->1->5 assert!(problem.evaluate(&vec![0, 2, 4, 3, 1, 5]).unwrap()); } @@ -83,10 +87,9 @@ fn test_hamiltonian_path_nontrivial() { #[test] fn test_hamiltonian_path_complete_graph() { // Complete graph K4: every permutation is a Hamiltonian path - let problem = HamiltonianPath::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )); + let problem = HamiltonianPath::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), + ); let solver = BruteForce::new(); let all = solver.find_all_witnesses(&problem).unwrap(); // K4 has 4! = 24 Hamiltonian paths (all permutations) @@ -95,7 +98,7 @@ fn test_hamiltonian_path_complete_graph() { #[test] fn test_is_valid_hamiltonian_path_function() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); assert!(is_valid_hamiltonian_path(&graph, &[0, 1, 2, 3])); assert!(is_valid_hamiltonian_path(&graph, &[3, 2, 1, 0])); assert!(!is_valid_hamiltonian_path(&graph, &[0, 1, 3, 2])); @@ -107,7 +110,7 @@ fn test_is_valid_hamiltonian_path_function() { #[test] fn test_hamiltonian_path_serialization() { - let problem = HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let json = serde_json::to_value(&problem).unwrap(); let deserialized: HamiltonianPath = serde_json::from_value(json).unwrap(); assert_eq!(deserialized.num_vertices(), 3); @@ -116,14 +119,15 @@ fn test_hamiltonian_path_serialization() { #[test] fn test_is_valid_solution() { - let problem = HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); assert!(problem.is_valid_solution(&[0, 1, 2])); assert!(!problem.is_valid_solution(&[0, 2, 1])); // no edge 0-2 } #[test] fn test_parameter_getters() { - let problem = HamiltonianPath::new(SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)])); + let problem = + HamiltonianPath::new(SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap()); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 4); } @@ -133,19 +137,22 @@ fn test_hamiltonianpath_paper_example() { use crate::traits::Problem; // Paper/issue #217: 6 vertices, 8 edges - let problem = HamiltonianPath::new(SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (1, 3), - (2, 3), - (3, 4), - (3, 5), - (4, 2), - (5, 1), - ], - )); + let problem = HamiltonianPath::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 3), + (2, 3), + (3, 4), + (3, 5), + (4, 2), + (5, 1), + ], + ) + .unwrap(), + ); // Hamiltonian path: 0→2→4→3→1→5 assert!(problem.evaluate(&vec![0, 2, 4, 3, 1, 5]).unwrap()); @@ -164,7 +171,7 @@ fn test_single_vertex() { use crate::traits::Problem; // Single vertex graph: trivially has a Hamiltonian "path" (just the vertex) - let problem = HamiltonianPath::new(SimpleGraph::new(1, vec![])); + let problem = HamiltonianPath::new(SimpleGraph::new(1, vec![]).unwrap()); assert!(problem.evaluate(&vec![0]).unwrap()); let solver = BruteForce::new(); let all = solver.find_all_witnesses(&problem).unwrap(); diff --git a/src/unit_tests/models/graph/hamiltonian_path_between_two_vertices.rs b/src/unit_tests/models/graph/hamiltonian_path_between_two_vertices.rs index e2e22856f..c6173e696 100644 --- a/src/unit_tests/models/graph/hamiltonian_path_between_two_vertices.rs +++ b/src/unit_tests/models/graph/hamiltonian_path_between_two_vertices.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; #[test] @@ -9,15 +8,19 @@ fn test_hamiltonian_path_between_two_vertices_basic() { // Path graph: 0-1-2-3, source=0, target=3 let problem = HamiltonianPathBetweenTwoVertices::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), 0, 3, - ); + ) + .unwrap(); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); assert_eq!(problem.source_vertex(), 0); assert_eq!(problem.target_vertex(), 3); - assert_eq!(problem.dimensions(), vec![4, 4, 4, 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 4, 4, 4] + ); // Valid path: 0->1->2->3 assert!(problem.evaluate(&vec![0, 1, 2, 3]).unwrap()); @@ -35,10 +38,11 @@ fn test_hamiltonian_path_between_two_vertices_basic() { fn test_hamiltonian_path_between_two_vertices_no_solution() { // C5 cycle: s=0, t=2 has no Hamiltonian s-t path (from issue #831) let problem = HamiltonianPathBetweenTwoVertices::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(), 0, 2, - ); + ) + .unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); assert!( @@ -65,10 +69,12 @@ fn test_hamiltonian_path_between_two_vertices_brute_force() { (4, 5), (2, 3), ], - ), + ) + .unwrap(), 0, 5, - ); + ) + .unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); @@ -90,8 +96,12 @@ fn test_hamiltonian_path_between_two_vertices_brute_force() { #[test] fn test_hamiltonian_path_between_two_vertices_is_valid_solution() { - let problem = - HamiltonianPathBetweenTwoVertices::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 0, 2); + let problem = HamiltonianPathBetweenTwoVertices::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + 0, + 2, + ) + .unwrap(); assert!(problem.is_valid_solution(&[0, 1, 2])); assert!(!problem.is_valid_solution(&[2, 1, 0])); // wrong direction assert!(!problem.is_valid_solution(&[0, 2, 1])); // no edge 0-2 @@ -99,7 +109,7 @@ fn test_hamiltonian_path_between_two_vertices_is_valid_solution() { #[test] fn test_hamiltonian_path_between_two_vertices_is_valid_st_path_function() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); // Valid: 0->1->2->3 with source=0, target=3 assert!(is_valid_hamiltonian_st_path(&graph, &[0, 1, 2, 3], 0, 3)); // Invalid: reversed (source=3 but we pass source=0) @@ -114,8 +124,12 @@ fn test_hamiltonian_path_between_two_vertices_is_valid_st_path_function() { #[test] fn test_hamiltonian_path_between_two_vertices_serialization() { - let problem = - HamiltonianPathBetweenTwoVertices::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 0, 2); + let problem = HamiltonianPathBetweenTwoVertices::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + 0, + 2, + ) + .unwrap(); let json = serde_json::to_value(&problem).unwrap(); let deserialized: HamiltonianPathBetweenTwoVertices = serde_json::from_value(json).unwrap(); @@ -143,10 +157,12 @@ fn test_hamiltonian_path_between_two_vertices_paper_example() { (4, 5), (2, 3), ], - ), + ) + .unwrap(), 0, 5, - ); + ) + .unwrap(); // Issue-specified solution: 0 -> 3 -> 2 -> 1 -> 4 -> 5 assert!(problem.evaluate(&vec![0, 3, 2, 1, 4, 5]).unwrap()); diff --git a/src/unit_tests/models/graph/highly_connected_deletion.rs b/src/unit_tests/models/graph/highly_connected_deletion.rs index f0bffa6cb..37e509536 100644 --- a/src/unit_tests/models/graph/highly_connected_deletion.rs +++ b/src/unit_tests/models/graph/highly_connected_deletion.rs @@ -8,17 +8,20 @@ use crate::types::Min; // Canonical instance from the issue: // V = {0,1,2,3}, E = {(0,1),(0,2),(1,2),(2,3)} — triangle on {0,1,2} with leaf 3. fn canonical_problem() -> HighlyConnectedDeletion { - HighlyConnectedDeletion::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)])) + HighlyConnectedDeletion::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]).unwrap()) } // Discriminatory instance: a "double triangle" — two K3's joined by a bridge edge (0,3). // V = {0,1,2,3,4,5}, E = {(0,1),(0,2),(1,2),(0,3),(3,4),(3,5),(4,5)} — 7 edges. // Optimum: delete the bridge edge (0,3) at edge index 3 → two K3 components → value 1. fn double_triangle_problem() -> HighlyConnectedDeletion { - HighlyConnectedDeletion::new(SimpleGraph::new( - 6, - vec![(0, 1), (0, 2), (1, 2), (0, 3), (3, 4), (3, 5), (4, 5)], - )) + HighlyConnectedDeletion::new( + SimpleGraph::new( + 6, + vec![(0, 1), (0, 2), (1, 2), (0, 3), (3, 4), (3, 5), (4, 5)], + ) + .unwrap(), + ) } #[test] @@ -28,8 +31,11 @@ fn test_highly_connected_deletion_creation() { assert_eq!(problem.graph().num_edges(), 4); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 4); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); - assert_eq!(problem.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] diff --git a/src/unit_tests/models/graph/integral_flow_bundles.rs b/src/unit_tests/models/graph/integral_flow_bundles.rs index d8c7b3f79..dc5128579 100644 --- a/src/unit_tests/models/graph/integral_flow_bundles.rs +++ b/src/unit_tests/models/graph/integral_flow_bundles.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_requires_bundle_coverage() { assert!( @@ -21,24 +20,26 @@ use crate::traits::Problem; fn yes_instance() -> IntegralFlowBundles { IntegralFlowBundles::new( - DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]), + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]).unwrap(), 0, 3, vec![vec![0, 1], vec![2, 5], vec![3, 4]], vec![1, 1, 1], 1, ) + .unwrap() } fn no_instance() -> IntegralFlowBundles { IntegralFlowBundles::new( - DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]), + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]).unwrap(), 0, 3, vec![vec![0, 1], vec![2, 5], vec![3, 4]], vec![1, 1, 1], 2, ) + .unwrap() } fn satisfying_config() -> Vec { @@ -61,7 +62,10 @@ fn test_integral_flow_bundles_creation_and_getters() { #[test] fn test_integral_flow_bundles_dims_use_tight_arc_bounds() { let problem = yes_instance(); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2, 2, 2] + ); } #[test] 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 e15116232..4cc9f9aa2 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,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_capacities() { let problem = IntegralFlowHomologousArcs::try_from(IntegralFlowHomologousArcsCreateSpec { @@ -31,13 +30,14 @@ fn yes_instance() -> IntegralFlowHomologousArcs { (3, 5), (4, 5), ], - ); - IntegralFlowHomologousArcs::new(graph, vec![1; 8], 0, 5, 2, vec![(2, 5), (4, 3)]) + ) + .unwrap(); + IntegralFlowHomologousArcs::new(graph, vec![1; 8], 0, 5, 2, vec![(2, 5), (4, 3)]).unwrap() } fn no_instance() -> IntegralFlowHomologousArcs { - let graph = DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]); - IntegralFlowHomologousArcs::new(graph, vec![1; 4], 0, 3, 1, vec![(0, 1)]) + let graph = DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]).unwrap(); + IntegralFlowHomologousArcs::new(graph, vec![1; 4], 0, 3, 1, vec![(0, 1)]).unwrap() } #[test] @@ -50,7 +50,10 @@ fn test_integral_flow_homologous_arcs_creation() { assert_eq!(problem.requirement(), 2); assert_eq!(problem.max_capacity(), 1); assert_eq!(problem.homologous_pairs(), &[(2, 5), (4, 3)]); - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); } #[test] @@ -139,9 +142,13 @@ fn test_integral_flow_homologous_arcs_problem_name() { fn test_integral_flow_homologous_arcs_non_unit_capacity() { // s=0 -> 1 -> 2=t, with capacities [3, 3], homologous pair (0,1) so both arcs carry // equal flow. R=2 is satisfiable: f=[2,2]. - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = IntegralFlowHomologousArcs::new(graph, vec![3, 3], 0, 2, 2, vec![(0, 1)]); - assert_eq!(problem.dimensions(), vec![4, 4]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = + IntegralFlowHomologousArcs::new(graph, vec![3, 3], 0, 2, 2, vec![(0, 1)]).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 4] + ); assert_eq!(problem.max_capacity(), 3); assert!(problem.evaluate(&vec![2, 2]).unwrap()); assert!(problem.evaluate(&vec![3, 3]).unwrap()); 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 394d59128..0f3fb0793 100644 --- a/src/unit_tests/models/graph/integral_flow_with_multipliers.rs +++ b/src/unit_tests/models/graph/integral_flow_with_multipliers.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_zero_internal_multiplier() { assert!( @@ -37,7 +36,8 @@ fn yes_instance() -> IntegralFlowWithMultipliers { (5, 7), (6, 7), ], - ); + ) + .unwrap(); IntegralFlowWithMultipliers::new( graph, 0, @@ -46,6 +46,7 @@ fn yes_instance() -> IntegralFlowWithMultipliers { vec![1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4], 12, ) + .unwrap() } fn yes_config() -> Vec { @@ -53,8 +54,8 @@ fn yes_config() -> Vec { } fn no_instance() -> IntegralFlowWithMultipliers { - let graph = DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2)]); - IntegralFlowWithMultipliers::new(graph, 0, 3, vec![1, 2, 3, 1], vec![2, 1, 2, 5, 1], 7) + let graph = DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2)]).unwrap(); + IntegralFlowWithMultipliers::new(graph, 0, 3, vec![1, 2, 3, 1], vec![2, 1, 2, 5, 1], 7).unwrap() } #[test] @@ -69,7 +70,7 @@ fn test_integral_flow_with_multipliers_creation_accessors_and_dimensions() { assert_eq!(problem.multipliers(), &[1, 2, 3, 4, 5, 6, 4, 1]); assert_eq!(problem.capacities(), &[1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4]); assert_eq!( - problem.dimensions(), + crate::solvers::cartesian_dimensions(&problem).unwrap(), vec![2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 5] ); } diff --git a/src/unit_tests/models/graph/isomorphic_spanning_tree.rs b/src/unit_tests/models/graph/isomorphic_spanning_tree.rs index 3f7f244c5..22c78344e 100644 --- a/src/unit_tests/models/graph/isomorphic_spanning_tree.rs +++ b/src/unit_tests/models/graph/isomorphic_spanning_tree.rs @@ -1,18 +1,20 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; #[test] fn test_isomorphicspanningtree_basic() { // Triangle graph, path tree - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem: IsomorphicSpanningTree = IsomorphicSpanningTree::new(graph.clone(), tree.clone()); - assert_eq!(problem.dimensions(), vec![3, 3, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3, 3] + ); assert_eq!(problem.graph(), &graph); assert_eq!(problem.tree(), &tree); assert_eq!(problem.num_vertices(), 3); @@ -28,8 +30,8 @@ fn test_isomorphicspanningtree_basic() { fn test_isomorphicspanningtree_evaluation_yes() { // Host graph: 0-1, 1-2, 0-2 (triangle) // Tree: 0-1, 1-2 (path) - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = IsomorphicSpanningTree::new(graph, tree); // Identity mapping: π = [0, 1, 2] @@ -52,8 +54,8 @@ fn test_isomorphicspanningtree_evaluation_no() { // Host graph: path 0-1-2-3 (edges: 0-1, 1-2, 2-3) // Tree: star K_{1,3} center=0, leaves=1,2,3 (edges: 0-1, 0-2, 0-3) // No vertex in graph has degree 3, so no valid mapping exists - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); - let tree = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); + let tree = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(); let problem = IsomorphicSpanningTree::new(graph, tree); // No permutation should work @@ -64,8 +66,8 @@ fn test_isomorphicspanningtree_evaluation_no() { #[test] fn test_isomorphicspanningtree_invalid_configs() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = IsomorphicSpanningTree::new(graph, tree); // Not a permutation: repeated value @@ -85,8 +87,8 @@ fn test_isomorphicspanningtree_invalid_configs() { #[test] fn test_isomorphicspanningtree_solver_yes() { // Complete graph K4, any tree with 4 vertices should have a solution - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); - let tree = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); // path + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); + let tree = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); // path let problem = IsomorphicSpanningTree::new(graph, tree); let solver = BruteForce::new(); @@ -105,8 +107,8 @@ fn test_isomorphicspanningtree_solver_yes() { #[test] fn test_isomorphicspanningtree_solver_no() { // Path graph 0-1-2-3, star tree K_{1,3} - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); - let tree = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); + let tree = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(); let problem = IsomorphicSpanningTree::new(graph, tree); let solver = BruteForce::new(); @@ -119,8 +121,8 @@ fn test_isomorphicspanningtree_solver_no() { #[test] fn test_isomorphicspanningtree_serialization() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = IsomorphicSpanningTree::new(graph, tree); let json = serde_json::to_string(&problem).unwrap(); @@ -152,10 +154,11 @@ fn test_isomorphicspanningtree_caterpillar_example() { (5, 6), (1, 3), ], - ); + ) + .unwrap(); // Caterpillar tree: a-b, b-c, c-d, d-e, b-f, c-g // Using vertex indices: 0-1, 1-2, 2-3, 3-4, 1-5, 2-6 - let tree = SimpleGraph::new(7, vec![(0, 1), (1, 2), (2, 3), (3, 4), (1, 5), (2, 6)]); + let tree = SimpleGraph::new(7, vec![(0, 1), (1, 2), (2, 3), (3, 4), (1, 5), (2, 6)]).unwrap(); let problem = IsomorphicSpanningTree::new(graph, tree); // The issue gives solution: a→0, b→1, c→2, d→3, e→6, f→4, g→5 @@ -168,8 +171,8 @@ fn test_isomorphicspanningtree_paper_example() { // Paper example: G = K4, T = star S3 (center 0, leaves {1, 2, 3}) // Any bijection works since K4 has all edges. // Identity mapping π(i) = i embeds star edges {(0,1),(0,2),(0,3)} into K4. - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); - let tree = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); // star S3 + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); + let tree = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(); // star S3 let problem = IsomorphicSpanningTree::new(graph, tree); // Identity mapping: π = [0, 1, 2, 3] @@ -192,25 +195,25 @@ fn test_isomorphicspanningtree_variant() { #[test] #[should_panic(expected = "graph and tree must have the same number of vertices")] fn test_isomorphicspanningtree_mismatched_sizes() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let tree = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let tree = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); IsomorphicSpanningTree::new(graph, tree); } #[test] #[should_panic(expected = "tree must have exactly n-1 edges")] fn test_isomorphicspanningtree_not_a_tree() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); // Not a tree: 3 edges for 3 vertices (has a cycle) - let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); IsomorphicSpanningTree::new(graph, tree); } #[test] #[should_panic(expected = "tree must be connected")] fn test_isomorphicspanningtree_disconnected_tree() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]); - let tree = SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]).unwrap(); + let tree = SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); IsomorphicSpanningTree::new(graph, tree); } diff --git a/src/unit_tests/models/graph/kclique.rs b/src/unit_tests/models/graph/kclique.rs index 910275599..f5a903426 100644 --- a/src/unit_tests/models/graph/kclique.rs +++ b/src/unit_tests/models/graph/kclique.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_k_above_vertex_count() { assert!(KClique::try_from(KCliqueCreateSpec { @@ -14,7 +13,7 @@ use crate::topology::SimpleGraph; use crate::traits::Problem; fn issue_graph() -> SimpleGraph { - SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]) + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap() } fn issue_witness() -> Vec { @@ -23,19 +22,22 @@ fn issue_witness() -> Vec { #[test] fn test_kclique_creation() { - let problem = KClique::new(issue_graph(), 3); + let problem = KClique::new(issue_graph(), 3).unwrap(); assert_eq!(problem.graph().num_vertices(), 5); assert_eq!(problem.graph().num_edges(), 6); assert_eq!(problem.k(), 3); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 6); - assert_eq!(problem.dimensions(), vec![2; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); } #[test] fn test_kclique_evaluate_yes_instance() { - let problem = KClique::new(issue_graph(), 3); + let problem = KClique::new(issue_graph(), 3).unwrap(); assert!(problem.evaluate(&issue_witness()).unwrap()); assert!(problem.is_valid_solution(&issue_witness())); @@ -43,7 +45,7 @@ fn test_kclique_evaluate_yes_instance() { #[test] fn test_kclique_evaluate_rejects_non_clique() { - let problem = KClique::new(issue_graph(), 3); + let problem = KClique::new(issue_graph(), 3).unwrap(); assert!(!problem .evaluate(&vec![true, false, true, true, false]) @@ -53,7 +55,7 @@ fn test_kclique_evaluate_rejects_non_clique() { #[test] fn test_kclique_evaluate_rejects_too_small_clique() { - let problem = KClique::new(issue_graph(), 3); + let problem = KClique::new(issue_graph(), 3).unwrap(); assert!(!problem .evaluate(&vec![true, false, true, false, false]) @@ -65,7 +67,7 @@ fn test_kclique_evaluate_rejects_too_small_clique() { #[test] fn test_kclique_solver_finds_unique_witness() { - let problem = KClique::new(issue_graph(), 3); + let problem = KClique::new(issue_graph(), 3).unwrap(); let solver = BruteForce::new(); assert_eq!(solver.solve(&problem).unwrap(), Some(issue_witness())); @@ -77,7 +79,7 @@ fn test_kclique_solver_finds_unique_witness() { #[test] fn test_kclique_serialization_round_trip() { - let problem = KClique::new(issue_graph(), 3); + let problem = KClique::new(issue_graph(), 3).unwrap(); let json = serde_json::to_string(&problem).unwrap(); let restored: KClique = serde_json::from_str(&json).unwrap(); @@ -88,7 +90,7 @@ fn test_kclique_serialization_round_trip() { #[test] fn test_kclique_paper_example() { - let problem = KClique::new(issue_graph(), 3); + let problem = KClique::new(issue_graph(), 3).unwrap(); let solver = BruteForce::new(); assert!(problem.evaluate(&issue_witness()).unwrap()); @@ -100,7 +102,7 @@ fn test_kclique_paper_example() { #[test] fn test_kclique_config_from_selected_vertices() { - let problem = KClique::new(issue_graph(), 3); + let problem = KClique::new(issue_graph(), 3).unwrap(); assert_eq!( problem.config_from_selected_vertices(&[2, 3, 4]), diff --git a/src/unit_tests/models/graph/kcoloring.rs b/src/unit_tests/models/graph/kcoloring.rs index 588c5721e..d13639e02 100644 --- a/src/unit_tests/models/graph/kcoloring.rs +++ b/src/unit_tests/models/graph/kcoloring.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_specs_separate_runtime_and_fixed_color_counts() { @@ -22,8 +21,8 @@ fn create_specs_separate_runtime_and_fixed_color_counts() { #[test] fn fixed_and_runtime_variants_report_num_colors_parameter() { - let fixed = KColoring::::new(SimpleGraph::new(2, vec![(0, 1)])); - let runtime = KColoring::::with_k(SimpleGraph::new(2, vec![(0, 1)]), 5); + let fixed = KColoring::::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap()); + let runtime = KColoring::::with_k(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 5); assert_eq!(Problem::parameters(&fixed).get("num_colors"), Some(3)); assert_eq!(Problem::parameters(&runtime).get("num_colors"), Some(5)); @@ -32,25 +31,29 @@ fn fixed_and_runtime_variants_report_num_colors_parameter() { as Problem>::parameter_names() ); } +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::variant::{K1, K2, K3, K4}; -include!("../../jl_helpers.rs"); #[test] fn test_kcoloring_creation() { - let problem = KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = + KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); assert_eq!(problem.num_colors(), 3); - assert_eq!(problem.dimensions(), vec![3, 3, 3, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3, 3, 3] + ); } #[test] fn test_evaluate_valid() { use crate::traits::Problem; - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); // Valid: different colors on adjacent vertices assert!(problem.evaluate(&vec![0, 1, 0]).unwrap()); @@ -61,7 +64,7 @@ fn test_evaluate_valid() { fn test_evaluate_invalid() { use crate::traits::Problem; - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); // Invalid: adjacent vertices have same color assert!(!problem.evaluate(&vec![0, 0, 1]).unwrap()); @@ -73,7 +76,8 @@ fn test_brute_force_path() { use crate::traits::Problem; // Path graph can be 2-colored - let problem = KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = + KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -88,7 +92,8 @@ fn test_brute_force_triangle() { use crate::traits::Problem; // Triangle needs 3 colors - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let problem = + KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -104,7 +109,8 @@ fn test_brute_force_triangle() { #[test] fn test_triangle_2_colors() { // Triangle cannot be 2-colored - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let problem = + KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -114,7 +120,7 @@ fn test_triangle_2_colors() { #[test] fn test_is_valid_coloring_function() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); assert!(is_valid_coloring(&graph, &[0, 1, 0], 2)); assert!(is_valid_coloring(&graph, &[0, 1, 2], 3)); @@ -126,7 +132,7 @@ fn test_is_valid_coloring_function() { #[test] #[should_panic(expected = "coloring length must match num_vertices")] fn test_is_valid_coloring_wrong_len() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); is_valid_coloring(&graph, &[0, 1], 2); // Wrong length } @@ -134,7 +140,7 @@ fn test_is_valid_coloring_wrong_len() { fn test_empty_graph() { use crate::traits::Problem; - let problem = KColoring::::new(SimpleGraph::new(3, vec![])); + let problem = KColoring::::new(SimpleGraph::new(3, vec![]).unwrap()); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -150,10 +156,9 @@ fn test_complete_graph_k4() { use crate::traits::Problem; // K4 needs 4 colors - let problem = KColoring::::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )); + let problem = KColoring::::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), + ); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -164,7 +169,7 @@ fn test_complete_graph_k4() { #[test] fn test_new_from_graph() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = KColoring::::new(graph); assert_eq!(problem.graph().num_vertices(), 3); assert_eq!(problem.graph().num_edges(), 2); @@ -175,8 +180,11 @@ fn test_kcoloring_problem() { use crate::traits::Problem; // Triangle graph with 3 colors - let p = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); - assert_eq!(p.dimensions(), vec![3, 3, 3]); + let p = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![3, 3, 3] + ); // Valid: each vertex different color assert!(p.evaluate(&vec![0, 1, 2]).unwrap()); // Invalid: vertices 0 and 1 same color @@ -191,7 +199,7 @@ fn test_jl_parity_evaluation() { let nv = instance["instance"]["num_vertices"].as_u64().unwrap() as usize; let edges = jl_parse_edges(&instance["instance"]); let num_edges = edges.len(); - let problem = KColoring::::new(SimpleGraph::new(nv, edges)); + let problem = KColoring::::new(SimpleGraph::new(nv, edges).unwrap()); for eval in instance["evaluations"].as_array().unwrap() { let config = jl_parse_config(&eval["config"]); let result = problem.evaluate(&config).unwrap().0; @@ -213,7 +221,7 @@ fn test_jl_parity_evaluation() { #[test] fn test_is_valid_solution() { // Path graph: 0-1-2, 3-coloring - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); // Valid: neighbors have different colors assert!(problem.is_valid_solution(&[0, 1, 0])); // Invalid: adjacent vertices 0 and 1 have same color @@ -222,7 +230,7 @@ fn test_is_valid_solution() { #[test] fn test_parameter_getters() { - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_edges(), 2); } @@ -231,13 +239,13 @@ fn test_parameter_getters() { fn test_kcoloring_paper_example() { use crate::traits::Problem; // Paper: house graph, k=3, proper coloring [0,1,1,0,2], chi(G)=3 - let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(); let problem = KColoring::::new(graph); let config = vec![0, 1, 1, 0, 2]; assert!(problem.evaluate(&config).unwrap()); // Verify not 2-colorable (triangle v_2,v_3,v_4) - let graph2 = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); + let graph2 = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(); let problem2 = KColoring::::new(graph2); let solver = BruteForce::new(); assert!(solver.solve(&problem2).unwrap().is_none()); @@ -247,7 +255,7 @@ fn test_kcoloring_paper_example() { fn fixed_color_counts_survive_all_serialization_paths() { fn check() { let expected = K::K.unwrap(); - let source = KColoring::::new(SimpleGraph::new(2, vec![(0, 1)])); + let source = KColoring::::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap()); let mut data = serde_json::to_value(&source).unwrap(); let restored: KColoring = serde_json::from_value(data.clone()).unwrap(); assert_eq!(restored.num_colors(), expected); @@ -279,7 +287,7 @@ fn fixed_color_counts_survive_all_serialization_paths() { #[test] fn runtime_color_counts_keep_their_native_domain_on_deserialization() { for count in [0, 1, 3, 4, usize::MAX] { - let source = KColoring::::with_k(SimpleGraph::new(2, vec![(0, 1)]), count); + let source = KColoring::::with_k(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), count); let data = serde_json::to_value(&source).unwrap(); let restored: KColoring = serde_json::from_value(data).unwrap(); assert_eq!(restored.num_colors(), count); diff --git a/src/unit_tests/models/graph/kernel.rs b/src/unit_tests/models/graph/kernel.rs index 3e4ed36f7..a59ac642f 100644 --- a/src/unit_tests/models/graph/kernel.rs +++ b/src/unit_tests/models/graph/kernel.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -9,11 +8,15 @@ fn test_kernel_creation() { let graph = DirectedGraph::new( 5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 0), (4, 1)], - ); + ) + .unwrap(); let problem = Kernel::new(graph); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_arcs(), 7); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2, 2] + ); } #[test] @@ -25,7 +28,8 @@ fn test_kernel_evaluate_valid() { let graph = DirectedGraph::new( 5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 0), (4, 1)], - ); + ) + .unwrap(); let problem = Kernel::new(graph); assert_eq!( problem @@ -41,7 +45,8 @@ fn test_kernel_evaluate_not_independent() { let graph = DirectedGraph::new( 5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 0), (4, 1)], - ); + ) + .unwrap(); let problem = Kernel::new(graph); assert_eq!( problem @@ -62,7 +67,8 @@ fn test_kernel_evaluate_not_absorbing() { let graph = DirectedGraph::new( 5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 0), (4, 1)], - ); + ) + .unwrap(); let problem = Kernel::new(graph); assert_eq!( problem @@ -77,7 +83,8 @@ fn test_kernel_brute_force() { let graph = DirectedGraph::new( 5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 0), (4, 1)], - ); + ) + .unwrap(); let problem = Kernel::new(graph); let solver = BruteForce::new(); let solution = solver @@ -95,7 +102,7 @@ fn test_kernel_no_solution() { // Wait, let's verify: {0}: successors of 1 = {2}, not selected. Not absorbing. // {0,1}: arc (0,1) exists. Not independent. // No kernel exists for odd cycles. - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); let problem = Kernel::new(graph); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); @@ -103,7 +110,7 @@ fn test_kernel_no_solution() { #[test] fn test_kernel_serialization() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = Kernel::new(graph); let json = serde_json::to_value(&problem).unwrap(); let deserialized: Kernel = serde_json::from_value(json).unwrap(); @@ -116,7 +123,7 @@ fn test_kernel_empty_graph() { // A graph with no arcs: every vertex is independent; absorption requires // every unselected vertex to have an arc to a selected one, but no arcs exist. // So the only kernel is the full vertex set (all selected → no unselected vertices to check). - let graph = DirectedGraph::new(3, vec![]); + let graph = DirectedGraph::new(3, vec![]).unwrap(); let problem = Kernel::new(graph); // All selected: independent (no arcs), absorbing (no unselected vertices) assert_eq!( 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 bb266893d..f4260f973 100644 --- a/src/unit_tests/models/graph/kth_best_spanning_tree.rs +++ b/src/unit_tests/models/graph/kth_best_spanning_tree.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -9,20 +8,20 @@ use crate::traits::Problem; /// {01,02,03} (star at 0, w=4) and {01,02,13} (w=4). /// Satisfying configs = 2 (the two orderings). fn yes_instance() -> KthBestSpanningTree { - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); - KthBestSpanningTree::::new(graph, vec![1, 1, 2, 2, 2, 3], 2, 4) + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); + KthBestSpanningTree::::new(graph, vec![1, 1, 2, 2, 2, 3], 2, 4).unwrap() } fn no_instance() -> KthBestSpanningTree { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let weights = vec![1, 1, 1]; - KthBestSpanningTree::::new(graph, weights, 2, 3) + KthBestSpanningTree::::new(graph, weights, 2, 3).unwrap() } fn small_yes_instance() -> KthBestSpanningTree { - let graph = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); let weights = vec![1, 1, 1]; - KthBestSpanningTree::::new(graph, weights, 2, 2) + KthBestSpanningTree::::new(graph, weights, 2, 2).unwrap() } /// Star at 0: edges {01,02,03}, then {01,02,13}. @@ -37,7 +36,10 @@ fn yes_witness_config() -> Vec> { fn test_kthbestspanningtree_creation() { let problem = yes_instance(); - assert_eq!(problem.dimensions(), vec![2; 12]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 12] + ); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 6); assert_eq!(problem.num_vertices(), 4); @@ -144,7 +146,9 @@ fn test_kthbestspanningtree_serialization() { #[test] fn test_kthbestspanningtree_single_vertex_accepts_single_empty_tree() { - let problem = KthBestSpanningTree::::new(SimpleGraph::new(1, vec![]), vec![], 1, 0); + let problem = + KthBestSpanningTree::::new(SimpleGraph::new(1, vec![]).unwrap(), vec![], 1, 0) + .unwrap(); let config = vec![Vec::::new()]; assert!(problem.evaluate(&config).unwrap()); assert!(problem.is_valid_solution(&config).unwrap()); @@ -152,22 +156,25 @@ fn test_kthbestspanningtree_single_vertex_accepts_single_empty_tree() { #[test] fn test_kthbestspanningtree_single_vertex_rejects_multiple_empty_trees() { - let problem = KthBestSpanningTree::::new(SimpleGraph::new(1, vec![]), vec![], 2, 0); + let problem = + KthBestSpanningTree::::new(SimpleGraph::new(1, vec![]).unwrap(), vec![], 2, 0) + .unwrap(); let config = vec![Vec::::new(), Vec::::new()]; assert!(!problem.evaluate(&config).unwrap()); } #[test] -#[should_panic(expected = "weights length must match graph num_edges")] fn test_kthbestspanningtree_creation_rejects_weight_length_mismatch() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let _ = KthBestSpanningTree::::new(graph, vec![1], 1, 2); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + assert!(KthBestSpanningTree::::new(graph, vec![1], 1, 2).is_err()); } #[test] -#[should_panic(expected = "k must be positive")] fn test_kthbestspanningtree_creation_rejects_zero_k() { - let _ = KthBestSpanningTree::::new(SimpleGraph::new(1, vec![]), vec![], 0, 0); + assert!( + KthBestSpanningTree::::new(SimpleGraph::new(1, vec![]).unwrap(), vec![], 0, 0) + .is_err() + ); } #[test] fn create_spec_maps_edge_weights_to_weights() { 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 619d0d892..959785c32 100644 --- a/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs +++ b/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs @@ -19,12 +19,12 @@ use crate::traits::Problem; use crate::types::Max; fn sample_graph() -> SimpleGraph { - SimpleGraph::new(5, vec![(0, 1), (1, 4), (0, 2), (2, 4), (0, 3), (3, 4)]) + SimpleGraph::new(5, vec![(0, 1), (1, 4), (0, 2), (2, 4), (0, 3), (3, 4)]).unwrap() } fn sample_problem() -> LengthBoundedDisjointPaths { // max_paths = min(deg(0), deg(4)) = min(3, 3) = 3 - LengthBoundedDisjointPaths::new(sample_graph(), 0, 4, 3) + LengthBoundedDisjointPaths::new(sample_graph(), 0, 4, 3).unwrap() } #[test] @@ -35,38 +35,37 @@ fn test_length_bounded_disjoint_paths_creation() { assert_eq!(problem.max_paths(), 3); assert_eq!(problem.max_length(), 3); // 3 slots * 6 edges = 18 binary variables - assert_eq!(problem.dimensions(), vec![2; 18]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 18] + ); } #[test] fn test_length_bounded_disjoint_paths_allows_large_bounds() { - let problem = LengthBoundedDisjointPaths::new(sample_graph(), 0, 4, 10); + let problem = LengthBoundedDisjointPaths::new(sample_graph(), 0, 4, 10).unwrap(); let config = encode_paths(6, 3, &[&[0, 1], &[2, 3]]); assert_eq!(problem.evaluate(&config).unwrap(), Max(Some(2))); } #[test] -#[should_panic(expected = "source must be a valid graph vertex")] fn test_length_bounded_disjoint_paths_creation_rejects_invalid_source() { - let _ = LengthBoundedDisjointPaths::new(sample_graph(), 5, 4, 3); + assert!(LengthBoundedDisjointPaths::new(sample_graph(), 5, 4, 3).is_err()); } #[test] -#[should_panic(expected = "sink must be a valid graph vertex")] fn test_length_bounded_disjoint_paths_creation_rejects_invalid_sink() { - let _ = LengthBoundedDisjointPaths::new(sample_graph(), 0, 5, 3); + assert!(LengthBoundedDisjointPaths::new(sample_graph(), 0, 5, 3).is_err()); } #[test] -#[should_panic(expected = "source and sink must be distinct")] fn test_length_bounded_disjoint_paths_creation_rejects_equal_terminals() { - let _ = LengthBoundedDisjointPaths::new(sample_graph(), 0, 0, 3); + assert!(LengthBoundedDisjointPaths::new(sample_graph(), 0, 0, 3).is_err()); } #[test] -#[should_panic(expected = "max_length must be positive")] fn test_length_bounded_disjoint_paths_creation_rejects_zero_bound() { - let _ = LengthBoundedDisjointPaths::new(sample_graph(), 0, 4, 0); + assert!(LengthBoundedDisjointPaths::new(sample_graph(), 0, 4, 0).is_err()); } #[test] @@ -120,9 +119,9 @@ fn test_length_bounded_disjoint_paths_rejects_disconnected_slot() { #[test] fn test_length_bounded_disjoint_paths_rejects_overlong_slot() { // Use a graph where a path has 3 edges but max_length=1 - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]).unwrap(); // max_paths = min(deg(0), deg(3)) = min(2, 2) = 2 - let problem = LengthBoundedDisjointPaths::new(graph, 0, 3, 1); + let problem = LengthBoundedDisjointPaths::new(graph, 0, 3, 1).unwrap(); // Path [0,1,2,3] has 3 edges but max_length=1 let config = encode_paths(4, 2, &[&[0, 1, 2]]); assert_eq!(problem.evaluate(&config).unwrap(), Max(None)); @@ -138,12 +137,19 @@ fn test_length_bounded_disjoint_paths_rejects_shared_internal_vertices() { #[test] fn test_length_bounded_disjoint_paths_rejects_reused_direct_edge() { - let problem = LengthBoundedDisjointPaths::new(SimpleGraph::new(2, vec![(0, 1)]), 0, 1, 1); + let problem = + LengthBoundedDisjointPaths::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 0, 1, 1) + .unwrap(); // max_paths = min(deg(0), deg(1)) = 1, so only 1 slot let config = encode_paths(1, 1, &[&[0]]); assert_eq!(problem.evaluate(&config).unwrap(), Max(Some(1))); - let triangle = - LengthBoundedDisjointPaths::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), 0, 2, 2); + let triangle = LengthBoundedDisjointPaths::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), + 0, + 2, + 2, + ) + .unwrap(); assert_eq!( triangle .evaluate(&encode_paths(3, 2, &[&[2], &[2]])) @@ -196,7 +202,7 @@ fn test_length_bounded_disjoint_paths_graph_getter() { #[test] fn test_length_bounded_disjoint_paths_num_variables() { let problem = sample_problem(); - assert_eq!(problem.num_variables(), 18); + assert_eq!(problem.num_variables().unwrap(), 18); } #[test] @@ -208,8 +214,13 @@ fn test_length_bounded_disjoint_paths_rejects_wrong_length_config() { #[test] fn test_length_bounded_disjoint_paths_chorded_path() { - let problem = - LengthBoundedDisjointPaths::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), 0, 2, 2); + let problem = LengthBoundedDisjointPaths::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), + 0, + 2, + 2, + ) + .unwrap(); let solution = encode_paths(3, 2, &[&[0, 1], &[2]]); assert_eq!(problem.evaluate(&solution).unwrap(), Max(Some(2))); let best = BruteForce::new().solve(&problem).unwrap().unwrap(); @@ -225,17 +236,19 @@ fn test_length_bounded_disjoint_paths_chorded_path() { #[test] fn test_length_bounded_disjoint_paths_rejects_disconnected_cycle() { let problem = LengthBoundedDisjointPaths::new( - SimpleGraph::new(5, vec![(0, 1), (2, 3), (3, 4), (4, 2)]), + SimpleGraph::new(5, vec![(0, 1), (2, 3), (3, 4), (4, 2)]).unwrap(), 0, 1, 4, - ); + ) + .unwrap(); assert_eq!(problem.evaluate(&vec![vec![true; 4]]).unwrap(), Max(None)); } #[test] fn test_length_bounded_disjoint_paths_edgeless_graph() { - let problem = LengthBoundedDisjointPaths::new(SimpleGraph::new(2, vec![]), 0, 1, 1); + let problem = + LengthBoundedDisjointPaths::new(SimpleGraph::new(2, vec![]).unwrap(), 0, 1, 1).unwrap(); let solution = BruteForce::new().solve(&problem).unwrap().unwrap(); assert!(solution.is_empty()); assert_eq!(problem.evaluate(&solution).unwrap(), Max(Some(0))); diff --git a/src/unit_tests/models/graph/longest_circuit.rs b/src/unit_tests/models/graph/longest_circuit.rs index 1789804b2..9ed6a1d97 100644 --- a/src/unit_tests/models/graph/longest_circuit.rs +++ b/src/unit_tests/models/graph/longest_circuit.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Max; @@ -21,9 +20,11 @@ fn issue_problem() -> LongestCircuit { (2, 5), (3, 5), ], - ), + ) + .unwrap(), vec![3, 2, 4, 1, 5, 2, 3, 2, 1, 2], ) + .unwrap() } #[test] @@ -32,7 +33,10 @@ fn test_longest_circuit_creation() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 10); assert_eq!(problem.edge_lengths(), &[3, 2, 4, 1, 5, 2, 3, 2, 1, 2]); - assert_eq!(problem.dimensions(), vec![2; 10]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 10] + ); assert!(problem.is_weighted()); } @@ -72,9 +76,10 @@ fn test_longest_circuit_evaluate_valid_and_invalid() { #[test] fn test_longest_circuit_rejects_disconnected_cycles() { let problem = LongestCircuit::new( - SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3)]), + SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3)]).unwrap(), vec![1, 1, 1, 1, 1, 1], - ); + ) + .unwrap(); assert_eq!( problem .evaluate(&vec![true, true, true, true, true, true]) @@ -126,22 +131,22 @@ fn test_longest_circuit_paper_example() { } #[test] -#[should_panic(expected = "All edge lengths must be positive (> 0)")] fn test_longest_circuit_rejects_non_positive_edge_lengths() { - LongestCircuit::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]), + assert!(LongestCircuit::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(), vec![1, 0, 1], - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "All edge lengths must be positive (> 0)")] fn test_longest_circuit_set_lengths_rejects_non_positive_values() { let mut problem = LongestCircuit::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(), vec![1, 1, 1], - ); - problem.set_lengths(vec![1, -2, 1]); + ) + .unwrap(); + assert!(problem.set_lengths(vec![1, -2, 1]).is_err()); } #[test] fn create_spec_maps_edge_weights_to_edge_lengths() { diff --git a/src/unit_tests/models/graph/longest_path.rs b/src/unit_tests/models/graph/longest_path.rs index f3d81c1c8..540d9c11b 100644 --- a/src/unit_tests/models/graph/longest_path.rs +++ b/src/unit_tests/models/graph/longest_path.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_nonpositive_lengths() { assert!(LongestPath::try_from(LongestPathI64CreateSpec { @@ -32,11 +31,13 @@ fn issue_problem() -> LongestPath { (5, 6), (1, 6), ], - ), + ) + .unwrap(), vec![3, 2, 4, 1, 5, 2, 3, 2, 4, 1], 0, 6, ) + .unwrap() } fn optimal_config() -> Vec { @@ -61,14 +62,17 @@ fn test_longest_path_creation() { assert_eq!(problem.num_edges(), 10); assert_eq!(problem.source_vertex(), 0); assert_eq!(problem.target_vertex(), 6); - assert_eq!(problem.dimensions(), vec![2; 10]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 10] + ); assert_eq!(problem.edge_lengths(), &[3, 2, 4, 1, 5, 2, 3, 2, 4, 1]); assert!(problem.is_weighted()); - problem.set_lengths(vec![1; 10]); + problem.set_lengths(vec![1; 10]).unwrap(); assert_eq!(problem.edge_lengths(), &[1; 10]); - let unweighted = LongestPath::new(SimpleGraph::path(4), vec![One; 3], 0, 3); + let unweighted = LongestPath::new(SimpleGraph::path(4), vec![One; 3], 0, 3).unwrap(); assert!(!unweighted.is_weighted()); } @@ -146,7 +150,7 @@ fn test_longest_path_serialization() { #[test] fn test_longest_path_source_equals_target_only_allows_empty_path() { - let problem = LongestPath::new(SimpleGraph::path(3), vec![5, 7], 1, 1); + let problem = LongestPath::new(SimpleGraph::path(3), vec![5, 7], 1, 1).unwrap(); assert!(problem.is_valid_solution(&[false, false])); assert_eq!(problem.evaluate(&vec![false, false]).unwrap(), Max(Some(0))); @@ -185,25 +189,21 @@ fn test_longest_path_problem_name() { } #[test] -#[should_panic(expected = "edge_lengths length must match num_edges")] fn test_longest_path_rejects_wrong_edge_lengths_len() { - LongestPath::new(SimpleGraph::path(3), vec![1], 0, 2); + assert!(LongestPath::new(SimpleGraph::path(3), vec![1], 0, 2).is_err()); } #[test] -#[should_panic(expected = "All edge lengths must be positive (> 0)")] fn test_longest_path_rejects_non_positive_edge_lengths() { - LongestPath::new(SimpleGraph::path(2), vec![0], 0, 1); + assert!(LongestPath::new(SimpleGraph::path(2), vec![0], 0, 1).is_err()); } #[test] -#[should_panic(expected = "source_vertex 3 out of bounds (graph has 3 vertices)")] fn test_longest_path_rejects_out_of_bounds_source() { - LongestPath::new(SimpleGraph::path(3), vec![1, 1], 3, 2); + assert!(LongestPath::new(SimpleGraph::path(3), vec![1, 1], 3, 2).is_err()); } #[test] -#[should_panic(expected = "target_vertex 3 out of bounds (graph has 3 vertices)")] fn test_longest_path_rejects_out_of_bounds_target() { - LongestPath::new(SimpleGraph::path(3), vec![1, 1], 0, 3); + assert!(LongestPath::new(SimpleGraph::path(3), vec![1, 1], 0, 3).is_err()); } diff --git a/src/unit_tests/models/graph/max_cut.rs b/src/unit_tests/models/graph/max_cut.rs index cb7497e77..1c853857e 100644 --- a/src/unit_tests/models/graph/max_cut.rs +++ b/src/unit_tests/models/graph/max_cut.rs @@ -1,30 +1,33 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; -include!("../../jl_helpers.rs"); #[test] fn test_maxcut_creation() { let problem = MaxCut::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1, 2, 3], - ); + ) + .unwrap(); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); } #[test] fn test_maxcut_unweighted() { - let problem = MaxCut::<_, i64>::unweighted(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = MaxCut::<_, i64>::unweighted(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); assert_eq!(problem.graph().num_edges(), 2); } #[test] fn test_cut_size_function() { use crate::topology::SimpleGraph; - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); let weights = vec![1, 2, 3]; // Partition {0} vs {1, 2} @@ -45,7 +48,11 @@ fn test_cut_size_function() { #[test] fn test_edge_weight() { - let problem = MaxCut::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![5, 10]); + let problem = MaxCut::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![5, 10], + ) + .unwrap(); assert_eq!(problem.edge_weight(0, 1), Some(&5)); assert_eq!(problem.edge_weight(1, 2), Some(&10)); assert_eq!(problem.edge_weight(0, 2), None); @@ -53,14 +60,22 @@ fn test_edge_weight() { #[test] fn test_edges() { - let problem = MaxCut::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 2]); + let problem = MaxCut::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1, 2], + ) + .unwrap(); let edges = problem.edges(); assert_eq!(edges.len(), 2); } #[test] fn test_new() { - let problem = MaxCut::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![5, 10]); + let problem = MaxCut::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![5, 10], + ) + .unwrap(); assert_eq!(problem.graph().num_vertices(), 3); assert_eq!(problem.graph().num_edges(), 2); assert_eq!(problem.edge_weights(), vec![5, 10]); @@ -68,7 +83,7 @@ fn test_new() { #[test] fn test_unweighted() { - let problem = MaxCut::<_, i64>::unweighted(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = MaxCut::<_, i64>::unweighted(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); assert_eq!(problem.graph().num_vertices(), 3); assert_eq!(problem.graph().num_edges(), 2); assert_eq!(problem.edge_weights(), vec![1, 1]); @@ -76,7 +91,7 @@ fn test_unweighted() { #[test] fn test_graph_accessor() { - let problem = MaxCut::<_, i64>::unweighted(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = MaxCut::<_, i64>::unweighted(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let graph = problem.graph(); assert_eq!(graph.num_vertices(), 3); assert_eq!(graph.num_edges(), 2); @@ -84,13 +99,21 @@ fn test_graph_accessor() { #[test] fn test_new_with_separate_weights() { - let problem = MaxCut::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![7, 3]); + let problem = MaxCut::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![7, 3], + ) + .unwrap(); assert_eq!(problem.edge_weights(), vec![7, 3]); } #[test] fn test_edge_weight_by_index() { - let problem = MaxCut::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![5, 10]); + let problem = MaxCut::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![5, 10], + ) + .unwrap(); assert_eq!(problem.edge_weight_by_index(0), Some(&5)); assert_eq!(problem.edge_weight_by_index(1), Some(&10)); assert_eq!(problem.edge_weight_by_index(2), None); @@ -105,7 +128,7 @@ fn test_jl_parity_evaluation() { let weighted_edges = jl_parse_weighted_edges(&instance["instance"]); let edges: Vec<(usize, usize)> = weighted_edges.iter().map(|&(u, v, _)| (u, v)).collect(); let weights: Vec = weighted_edges.into_iter().map(|(_, _, w)| w).collect(); - let problem = MaxCut::new(SimpleGraph::new(nv, edges), weights); + let problem = MaxCut::new(SimpleGraph::new(nv, edges).unwrap(), weights).unwrap(); for eval in instance["evaluations"].as_array().unwrap() { let config = jl_parse_bool_config(&eval["config"]); let result = problem.evaluate(&config).unwrap(); @@ -128,9 +151,10 @@ fn test_jl_parity_evaluation() { #[test] fn test_cut_size_method() { let problem = MaxCut::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1, 2, 3], - ); + ) + .unwrap(); // Partition {0} vs {1, 2}: cuts edges (0,1)=1 and (0,2)=3 assert_eq!(problem.cut_size(&[false, true, true]).unwrap(), 4); // All same partition: no edges cut @@ -139,7 +163,11 @@ fn test_cut_size_method() { #[test] fn test_parameter_getters() { - let problem = MaxCut::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 2]); + let problem = MaxCut::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 2], + ) + .unwrap(); assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_edges(), 2); } @@ -148,7 +176,7 @@ fn test_parameter_getters() { fn test_maxcut_paper_example() { use crate::traits::Problem; // Paper: house graph, S = {v_0, v_3}, cut = 5 - let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(); let problem = MaxCut::<_, i64>::unweighted(graph); let config = vec![true, false, false, true, false]; // S = {v_0, v_3} let result = problem.evaluate(&config).unwrap(); @@ -176,3 +204,11 @@ fn create_specs_use_edge_weights_for_both_weight_variants() { assert_eq!(unit.edge_weights(), vec![One]); assert_eq!(MaxCutI64CreateSpec::FIELDS[2].name, "edge_weights"); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); + assert!(MaxCut::new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "edge_weights": []}); + assert!(serde_json::from_value::>(json).is_err()); +} diff --git a/src/unit_tests/models/graph/maximal_is.rs b/src/unit_tests/models/graph/maximal_is.rs index f68ef7c18..0b2ef0722 100644 --- a/src/unit_tests/models/graph/maximal_is.rs +++ b/src/unit_tests/models/graph/maximal_is.rs @@ -5,45 +5,54 @@ use crate::solvers::BruteForceProblem as _; fn create_spec_rejects_weight_count_mismatch() { assert_eq!(MaximalISCreateSpec::FIELDS[1].name, "weights"); let result = MaximalIS::try_from(MaximalISCreateSpec { - graph: SimpleGraph::new(2, vec![(0, 1)]), + graph: SimpleGraph::new(2, vec![(0, 1)]).unwrap(), weights: vec![1], }); assert!(result.is_err()); } +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -include!("../../jl_helpers.rs"); #[test] fn test_maximal_is_creation() { let problem = MaximalIS::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); } #[test] fn test_maximal_is_with_weights() { - let problem = MaximalIS::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1, 2, 3]); + let problem = + MaximalIS::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1, 2, 3]).unwrap(); assert_eq!(problem.weights().to_vec(), vec![1, 2, 3]); assert!(problem.is_weighted()); } #[test] fn test_maximal_is_from_graph() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MaximalIS::new(graph, vec![1, 2, 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MaximalIS::new(graph, vec![1, 2, 3]).unwrap(); assert_eq!(problem.graph().num_vertices(), 3); assert_eq!(problem.weights().to_vec(), vec![1, 2, 3]); } #[test] fn test_is_independent() { - let problem = MaximalIS::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximalIS::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); assert!(problem.is_independent(&[true, false, true])); assert!(problem.is_independent(&[false, true, false])); @@ -52,7 +61,11 @@ fn test_is_independent() { #[test] fn test_is_maximal() { - let problem = MaximalIS::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximalIS::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); // {0, 2} is maximal (cannot add 1) assert!(problem.is_maximal(&[true, false, true])); @@ -69,7 +82,7 @@ fn test_is_maximal() { #[test] fn test_is_maximal_independent_set_function() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); assert!(is_maximal_independent_set(&graph, &[true, false, true])); assert!(is_maximal_independent_set(&graph, &[false, true, false])); @@ -79,33 +92,39 @@ fn test_is_maximal_independent_set_function() { #[test] fn test_weights() { - let problem = MaximalIS::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximalIS::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]).unwrap(); assert_eq!(problem.weights().to_vec(), vec![1, 1, 1]); // Unit weights } #[test] fn test_is_weighted() { // i64 type is always considered weighted - let problem = MaximalIS::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximalIS::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]).unwrap(); assert!(problem.is_weighted()); } #[test] fn test_is_weighted_empty() { // i64 type is always considered weighted, even with empty weights - let problem = MaximalIS::new(SimpleGraph::new(0, vec![]), vec![0i64; 0]); + let problem = MaximalIS::new(SimpleGraph::new(0, vec![]).unwrap(), vec![0i64; 0]).unwrap(); assert!(problem.is_weighted()); } #[test] #[should_panic(expected = "selected length must match num_vertices")] fn test_is_maximal_independent_set_wrong_len() { - is_maximal_independent_set(&SimpleGraph::new(3, vec![(0, 1)]), &[true, false]); + is_maximal_independent_set(&SimpleGraph::new(3, vec![(0, 1)]).unwrap(), &[true, false]); } #[test] fn test_graph_ref() { - let problem = MaximalIS::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximalIS::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); let graph = problem.graph(); assert_eq!(graph.num_vertices(), 3); assert_eq!(graph.num_edges(), 2); @@ -113,14 +132,22 @@ fn test_graph_ref() { #[test] fn test_edges() { - let problem = MaximalIS::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximalIS::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); let edges = problem.graph().edges(); assert_eq!(edges.len(), 2); } #[test] fn test_has_edge() { - let problem = MaximalIS::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximalIS::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); assert!(problem.graph().has_edge(0, 1)); assert!(problem.graph().has_edge(1, 0)); // Undirected assert!(problem.graph().has_edge(1, 2)); @@ -129,7 +156,8 @@ fn test_has_edge() { #[test] fn test_weights_ref() { - let problem = MaximalIS::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximalIS::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]).unwrap(); assert_eq!(problem.weights(), &[1, 1, 1]); } @@ -140,7 +168,7 @@ fn test_jl_parity_evaluation() { for instance in data["instances"].as_array().unwrap() { let nv = instance["instance"]["num_vertices"].as_u64().unwrap() as usize; let edges = jl_parse_edges(&instance["instance"]); - let problem = MaximalIS::new(SimpleGraph::new(nv, edges), vec![1i64; nv]); + let problem = MaximalIS::new(SimpleGraph::new(nv, edges).unwrap(), vec![1i64; nv]).unwrap(); for eval in instance["evaluations"].as_array().unwrap() { let config = jl_parse_bool_config(&eval["config"]); let result = problem.evaluate(&config).unwrap(); @@ -171,7 +199,11 @@ fn test_jl_parity_evaluation() { #[test] fn test_is_valid_solution() { // Path graph: 0-1-2 - let problem = MaximalIS::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximalIS::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); // Valid: {0, 2} is maximal (independent and no vertex can be added) assert!(problem.is_valid_solution(&[true, false, true])); // Invalid: {0} is independent but not maximal (vertex 2 can be added) @@ -181,9 +213,10 @@ fn test_is_valid_solution() { #[test] fn test_parameter_getters() { let problem = MaximalIS::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); } @@ -192,8 +225,8 @@ fn test_parameter_getters() { fn test_maximal_is_paper_example() { use crate::traits::Problem; // Paper: path P5, maximal IS {v_1, v_3} (weight 2), {v_0, v_2, v_4} (weight 3) - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); - let problem = MaximalIS::new(graph, vec![1i64; 5]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(); + let problem = MaximalIS::new(graph, vec![1i64; 5]).unwrap(); // {v_1, v_3} is maximal (can't add v_0: adj to v_1, can't add v_2: adj to both, can't add v_4: adj to v_3) let config1 = vec![false, true, false, true, false]; @@ -212,3 +245,16 @@ fn test_maximal_is_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap().unwrap(), 3); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::new(1, vec![]).unwrap(); + assert!(MaximalIS::new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "weights": []}); + assert!(serde_json::from_value::>(json.clone()).is_err()); + let variant = std::collections::BTreeMap::from([ + ("graph".into(), "SimpleGraph".into()), + ("weight".into(), "i64".into()), + ]); + assert!(crate::registry::load_dyn("MaximalIS", &variant, json).is_err()); +} diff --git a/src/unit_tests/models/graph/maximum_achromatic_number.rs b/src/unit_tests/models/graph/maximum_achromatic_number.rs index 9bafd0ea9..218f31e57 100644 --- a/src/unit_tests/models/graph/maximum_achromatic_number.rs +++ b/src/unit_tests/models/graph/maximum_achromatic_number.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Max; @@ -8,11 +7,14 @@ use crate::types::Max; #[test] fn test_maximum_achromatic_number_c6() { // C6 (6-cycle): achromatic number is 3 - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)]); + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)]).unwrap(); let problem = MaximumAchromaticNumber::new(graph); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 6); - assert_eq!(problem.dimensions(), vec![6; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6; 6] + ); // [0,1,2,0,1,2] is a valid complete proper 3-coloring let config = vec![0, 1, 2, 0, 1, 2]; @@ -22,7 +24,7 @@ fn test_maximum_achromatic_number_c6() { #[test] fn test_maximum_achromatic_number_improper_coloring() { // Adjacent vertices with the same color -> invalid - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = MaximumAchromaticNumber::new(graph); // Vertices 0 and 1 are adjacent and share color 0 @@ -32,7 +34,7 @@ fn test_maximum_achromatic_number_improper_coloring() { #[test] fn test_maximum_achromatic_number_incomplete_coloring() { // Proper but not complete: color pair with no connecting edge -> invalid - let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(); let problem = MaximumAchromaticNumber::new(graph); // Colors: 0->0, 1->1, 2->2, 3->3 — proper (no adjacent same color) @@ -50,7 +52,7 @@ fn test_maximum_achromatic_number_solver() { // Possible colorings: // [0,1,0] -> 2 colors, proper, complete (edge between 0 and 1 classes) -> Max(2) // [0,1,2] -> 3 colors, proper, but colors 0 and 2 have no edge -> incomplete - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MaximumAchromaticNumber::new(graph); let solver = BruteForce::new(); @@ -61,7 +63,7 @@ fn test_maximum_achromatic_number_solver() { #[test] fn test_maximum_achromatic_number_wrong_length() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MaximumAchromaticNumber::new(graph); assert!(matches!( problem.evaluate(&vec![0, 1]), @@ -72,7 +74,7 @@ fn test_maximum_achromatic_number_wrong_length() { #[test] fn test_maximum_achromatic_number_empty_graph() { // No vertices, no edges - let graph = SimpleGraph::new(0, vec![]); + let graph = SimpleGraph::new(0, vec![]).unwrap(); let problem = MaximumAchromaticNumber::new(graph); assert_eq!(problem.evaluate(&vec![]).unwrap(), Max(Some(0))); } @@ -80,7 +82,7 @@ fn test_maximum_achromatic_number_empty_graph() { #[test] fn test_maximum_achromatic_number_single_vertex() { // Single vertex, no edges: 1 color, trivially complete - let graph = SimpleGraph::new(1, vec![]); + let graph = SimpleGraph::new(1, vec![]).unwrap(); let problem = MaximumAchromaticNumber::new(graph); assert_eq!(problem.evaluate(&vec![0]).unwrap(), Max(Some(1))); } @@ -88,7 +90,7 @@ fn test_maximum_achromatic_number_single_vertex() { #[test] fn test_maximum_achromatic_number_complete_graph_k3() { // K3: achromatic number = 3 (each vertex gets its own color) - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); let problem = MaximumAchromaticNumber::new(graph); // 3 colors: proper and complete (every color pair has an edge) diff --git a/src/unit_tests/models/graph/maximum_clique.rs b/src/unit_tests/models/graph/maximum_clique.rs index f83c6107d..ded8991eb 100644 --- a/src/unit_tests/models/graph/maximum_clique.rs +++ b/src/unit_tests/models/graph/maximum_clique.rs @@ -1,11 +1,10 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_weight_count_mismatch() { assert_eq!(MaximumCliqueCreateSpec::::FIELDS[1].name, "weights"); let result = MaximumClique::try_from(MaximumCliqueCreateSpec { - graph: SimpleGraph::new(2, vec![(0, 1)]), + graph: SimpleGraph::new(2, vec![(0, 1)]).unwrap(), weights: vec![1], }); assert!(result.is_err()); @@ -17,17 +16,22 @@ use crate::types::{Max, One}; #[test] fn test_clique_creation() { let problem = MaximumClique::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); } #[test] fn test_clique_with_weights() { - let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1, 2, 3]); + let problem = + MaximumClique::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1, 2, 3]).unwrap(); assert_eq!(problem.weights().to_vec(), vec![1, 2, 3]); assert!(problem.is_weighted()); } @@ -35,13 +39,18 @@ fn test_clique_with_weights() { #[test] fn test_clique_unweighted() { // i64 type is always considered weighted, even with uniform values - let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximumClique::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]).unwrap(); assert!(problem.is_weighted()); } #[test] fn test_has_edge() { - let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximumClique::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); assert!(problem.graph().has_edge(0, 1)); assert!(problem.graph().has_edge(1, 0)); // Undirected assert!(problem.graph().has_edge(1, 2)); @@ -54,9 +63,10 @@ fn test_evaluate_valid() { // Complete graph K3 (triangle) let problem = MaximumClique::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); // Valid: all three form a clique assert_eq!( @@ -76,7 +86,11 @@ fn test_evaluate_invalid() { use crate::traits::Problem; // Path graph: 0-1-2 (no edge between 0 and 2) - let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximumClique::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); // Invalid: 0 and 2 are not adjacent - returns Invalid assert_eq!( @@ -95,7 +109,11 @@ fn test_evaluate_invalid() { fn test_evaluate_empty() { use crate::traits::Problem; - let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximumClique::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); // Empty set is a valid clique with size 0 assert_eq!( problem.evaluate(&vec![false, false, false]).unwrap(), @@ -108,9 +126,10 @@ fn test_weighted_solution() { use crate::traits::Problem; let problem = MaximumClique::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![10, 20, 30], - ); + ) + .unwrap(); // Select vertex 2 (weight 30) assert_eq!( @@ -129,9 +148,10 @@ fn test_weighted_solution() { fn test_brute_force_triangle() { // Triangle graph (K3): max clique is all 3 vertices let problem = MaximumClique::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -144,7 +164,11 @@ fn test_brute_force_path() { use crate::traits::Problem; // Path graph 0-1-2: max clique is any adjacent pair - let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximumClique::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -162,7 +186,11 @@ fn test_brute_force_weighted() { use crate::traits::Problem; // Path with weights: vertex 1 has high weight - let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 100, 1]); + let problem = MaximumClique::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1, 100, 1], + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -177,28 +205,32 @@ fn test_brute_force_weighted() { fn test_is_clique_function() { // Triangle assert!(is_clique( - &SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), &[true, true, true] )); assert!(is_clique( - &SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), &[true, true, false] )); // Path - not all pairs adjacent assert!(!is_clique( - &SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), &[true, false, true] )); assert!(is_clique( - &SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), &[true, true, false] )); // Adjacent pair } #[test] fn test_edges() { - let problem = MaximumClique::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), vec![1i64; 4]); + let problem = MaximumClique::new( + SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(), + vec![1i64; 4], + ) + .unwrap(); let edges = problem.graph().edges(); assert_eq!(edges.len(), 2); } @@ -206,7 +238,7 @@ fn test_edges() { #[test] fn test_empty_graph() { // No edges means any single vertex is a max clique - let problem = MaximumClique::new(SimpleGraph::new(3, vec![]), vec![1i64; 3]); + let problem = MaximumClique::new(SimpleGraph::new(3, vec![]).unwrap(), vec![1i64; 3]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -221,7 +253,11 @@ fn test_empty_graph() { fn test_is_clique_method() { use crate::traits::Problem; - let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximumClique::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); // Valid clique - returns Valid assert!(problem @@ -241,15 +277,16 @@ fn test_is_clique_method() { #[test] fn test_from_graph() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MaximumClique::new(graph, vec![1, 2, 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MaximumClique::new(graph, vec![1, 2, 3]).unwrap(); assert_eq!(problem.graph().num_vertices(), 3); assert_eq!(problem.weights().to_vec(), vec![1, 2, 3]); } #[test] fn test_graph_accessor() { - let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximumClique::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]).unwrap(); let graph = problem.graph(); assert_eq!(graph.num_vertices(), 3); assert_eq!(graph.num_edges(), 1); @@ -257,7 +294,8 @@ fn test_graph_accessor() { #[test] fn test_weights_ref() { - let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1)]), vec![5, 10, 15]); + let problem = + MaximumClique::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![5, 10, 15]).unwrap(); assert_eq!(problem.weights(), &[5, 10, 15]); } @@ -265,16 +303,17 @@ fn test_weights_ref() { #[should_panic(expected = "selected length must match num_vertices")] fn test_is_clique_wrong_len() { // Wrong length should panic - is_clique(&SimpleGraph::new(3, vec![(0, 1)]), &[true, false]); + is_clique(&SimpleGraph::new(3, vec![(0, 1)]).unwrap(), &[true, false]); } #[test] fn test_complete_graph() { // K4 - complete graph with 4 vertices let problem = MaximumClique::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -288,10 +327,14 @@ fn test_clique_problem() { // Triangle graph: all pairs connected let p = MaximumClique::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], + ) + .unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] ); - assert_eq!(p.dimensions(), vec![2, 2, 2]); // Valid clique: select all 3 vertices (triangle is a clique) assert_eq!(p.evaluate(&vec![true, true, true]).unwrap(), Max(Some(3))); // Valid clique: select just vertex 0 @@ -302,20 +345,29 @@ fn test_clique_problem() { fn test_is_valid_solution() { // Triangle: 0-1-2 all connected let problem = MaximumClique::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); // Valid: all three form a clique assert!(problem.is_valid_solution(&[true, true, true])); // Now path graph: 0-1-2 (no 0-2 edge) - let problem2 = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem2 = MaximumClique::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); // Invalid: {0, 2} not adjacent assert!(!problem2.is_valid_solution(&[true, false, true])); } #[test] fn test_parameter_getters() { - let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximumClique::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_edges(), 2); } @@ -326,9 +378,10 @@ fn test_clique_one_weights_evaluate_and_solve() { // Triangle with unit weights: max clique covers all 3 vertices. let problem = MaximumClique::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![One; 3], - ); + ) + .unwrap(); assert!(!problem.is_weighted()); assert_eq!( problem.evaluate(&vec![true, true, true]).unwrap(), @@ -340,7 +393,11 @@ fn test_clique_one_weights_evaluate_and_solve() { ); // Invalid clique on this graph? K3 is complete, so every subset is a clique. // Re-verify invalidity on a path graph: - let path = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![One; 3]); + let path = MaximumClique::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![One; 3], + ) + .unwrap(); assert_eq!(path.evaluate(&vec![true, false, true]).unwrap(), Max(None)); let solver = BruteForce::new(); @@ -353,8 +410,8 @@ fn test_clique_one_weights_evaluate_and_solve() { fn test_clique_paper_example() { use crate::traits::Problem; // Paper: house graph, max clique K = {v_2, v_3, v_4}, omega(G) = 3 - let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); - let problem = MaximumClique::new(graph, vec![1i64; 5]); + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(); + let problem = MaximumClique::new(graph, vec![1i64; 5]).unwrap(); let config = vec![false, false, true, true, true]; // {v_2, v_3, v_4} let result = problem.evaluate(&config).unwrap(); assert!(result.is_valid()); @@ -364,3 +421,16 @@ fn test_clique_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap().unwrap(), 3); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::new(1, vec![]).unwrap(); + assert!(MaximumClique::new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "weights": []}); + assert!(serde_json::from_value::>(json.clone()).is_err()); + let variant = std::collections::BTreeMap::from([ + ("graph".into(), "SimpleGraph".into()), + ("weight".into(), "i64".into()), + ]); + assert!(crate::registry::load_dyn("MaximumClique", &variant, json).is_err()); +} diff --git a/src/unit_tests/models/graph/maximum_co_k_plex.rs b/src/unit_tests/models/graph/maximum_co_k_plex.rs index f92e1eaf2..8ce8527e3 100644 --- a/src/unit_tests/models/graph/maximum_co_k_plex.rs +++ b/src/unit_tests/models/graph/maximum_co_k_plex.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::{Max, One}; @@ -10,7 +9,7 @@ use crate::variant::KN; fn create_spec_uses_k_input() { assert_eq!(MaximumCoKPlexCreateSpec::::FIELDS[2].name, "k"); let problem = MaximumCoKPlex::try_from(MaximumCoKPlexCreateSpec { - graph: SimpleGraph::new(2, vec![(0, 1)]), + graph: SimpleGraph::new(2, vec![(0, 1)]).unwrap(), weights: vec![2, 3], k: 1, }) @@ -20,11 +19,11 @@ fn create_spec_uses_k_input() { } fn c5() -> SimpleGraph { - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]) + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap() } fn issue_instance() -> MaximumCoKPlex { - MaximumCoKPlex::<_, i64, KN>::with_k(c5(), vec![5, 1, 4, 1, 3], 2) + MaximumCoKPlex::<_, i64, KN>::with_k(c5(), vec![5, 1, 4, 1, 3], 2).unwrap() } #[test] @@ -34,7 +33,10 @@ fn test_maximum_co_k_plex_creation() { assert_eq!(problem.graph().num_edges(), 5); assert_eq!(problem.weights(), &[5, 1, 4, 1, 3]); assert_eq!(problem.bound_k(), 2); - assert_eq!(problem.dimensions(), vec![2; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 5); assert!(problem.is_weighted()); @@ -99,7 +101,7 @@ fn test_maximum_co_k_plex_brute_force() { fn test_maximum_co_k_plex_k_equals_1_is_independent_set() { // For k = 1 the co-k-plex constraint forces an independent set. // 5-cycle MIS has size 2, so unit-weight optimum is 2. - let problem = MaximumCoKPlex::<_, One, KN>::with_k(c5(), vec![One; 5], 1); + let problem = MaximumCoKPlex::<_, One, KN>::with_k(c5(), vec![One; 5], 1).unwrap(); let solver = BruteForce::new(); assert_eq!( problem @@ -154,15 +156,13 @@ fn test_maximum_co_k_plex_problem_name_and_variant() { } #[test] -#[should_panic(expected = "co-k-plex parameter k must be at least 1")] fn test_maximum_co_k_plex_rejects_zero_k() { - let _ = MaximumCoKPlex::<_, One, KN>::with_k(c5(), vec![One; 5], 0); + assert!(MaximumCoKPlex::<_, One, KN>::with_k(c5(), vec![One; 5], 0).is_err()); } #[test] -#[should_panic(expected = "weights length must match graph num_vertices")] fn test_maximum_co_k_plex_rejects_weight_length_mismatch() { - let _ = MaximumCoKPlex::<_, One, KN>::with_k(c5(), vec![One; 4], 2); + assert!(MaximumCoKPlex::<_, One, KN>::with_k(c5(), vec![One; 4], 2).is_err()); } #[test] @@ -187,3 +187,14 @@ fn test_maximum_co_k_plex_rejects_missing_bound_k_on_load() { "error should mention the missing field `bound_k`, got: {msg}" ); } + +#[test] +fn deserialize_checks_fixed_k_and_weight_count() { + for (weights, bound_k) in [(vec![1, 1], 2), (vec![1], 1)] { + assert!(serde_json::from_value::>( + serde_json::json!({ + "graph": {"num_vertices": 2, "edges": []}, "weights": weights, "bound_k": bound_k + }) + ).is_err()); + } +} diff --git a/src/unit_tests/models/graph/maximum_common_edge_subgraph.rs b/src/unit_tests/models/graph/maximum_common_edge_subgraph.rs index 00118979c..0a879cfe3 100644 --- a/src/unit_tests/models/graph/maximum_common_edge_subgraph.rs +++ b/src/unit_tests/models/graph/maximum_common_edge_subgraph.rs @@ -17,7 +17,8 @@ fn issue_instance() -> MaximumCommonEdgeSubgraph { LabelledArc::new(1, 3, 3), LabelledArc::new(3, 1, 4), ], - ), + ) + .unwrap(), LabelledDigraph::new( 4, vec![ @@ -28,7 +29,8 @@ fn issue_instance() -> MaximumCommonEdgeSubgraph { LabelledArc::new(1, 3, 3), LabelledArc::new(0, 1, 3), ], - ), + ) + .unwrap(), ) } @@ -41,8 +43,11 @@ fn test_maximum_common_edge_subgraph_creation() { assert_eq!(problem.num_arcs_2(), 6); assert_eq!(problem.bottom_index(), 4); // dims must be [|V2| + 1; |V1|] = [5; 5]. - assert_eq!(problem.dimensions(), vec![5; 5]); - assert_eq!(problem.num_variables(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 5] + ); + assert_eq!(problem.num_variables().unwrap(), 5); } #[test] @@ -170,15 +175,13 @@ fn test_maximum_common_edge_subgraph_rejects_out_of_range_target() { } #[test] -#[should_panic(expected = "labelled arc source")] fn test_labelled_digraph_rejects_out_of_range_source() { - let _ = LabelledDigraph::new(2, vec![LabelledArc::new(2, 0, 0)]); + assert!(LabelledDigraph::new(2, vec![LabelledArc::new(2, 0, 0)]).is_err()); } #[test] -#[should_panic(expected = "labelled arc destination")] fn test_labelled_digraph_rejects_out_of_range_destination() { - let _ = LabelledDigraph::new(2, vec![LabelledArc::new(0, 0, 2)]); + assert!(LabelledDigraph::new(2, vec![LabelledArc::new(0, 0, 2)]).is_err()); } #[test] @@ -190,6 +193,24 @@ fn test_labelled_digraph_deduplicates_arcs() { LabelledArc::new(0, 1, 2), LabelledArc::new(1, 0, 2), ], - ); + ) + .unwrap(); assert_eq!(g.num_arcs(), 2); } + +#[test] +fn deserialize_checks_and_normalizes_labelled_arcs() { + assert!( + serde_json::from_value::(serde_json::json!({ + "num_vertices": 2, "arcs": [{"src": 0, "label": 1, "dst": 2}] + })) + .is_err() + ); + let graph: LabelledDigraph = serde_json::from_value(serde_json::json!({ + "num_vertices": 2, "arcs": [ + {"src": 0, "label": 1, "dst": 1}, {"src": 0, "label": 1, "dst": 1} + ] + })) + .unwrap(); + assert_eq!(graph.arcs(), &[LabelledArc::new(0, 1, 1)]); +} diff --git a/src/unit_tests/models/graph/maximum_contact_map_overlap.rs b/src/unit_tests/models/graph/maximum_contact_map_overlap.rs index 03cb0e25d..2976a7617 100644 --- a/src/unit_tests/models/graph/maximum_contact_map_overlap.rs +++ b/src/unit_tests/models/graph/maximum_contact_map_overlap.rs @@ -20,8 +20,11 @@ fn test_maximum_contact_map_overlap_creation() { assert_eq!(problem.num_contacts_1(), 2); assert_eq!(problem.num_contacts_2(), 3); // dims must be [|V_2| + 1; |V_1|] = [6; 4]. - assert_eq!(problem.dimensions(), vec![6; 4]); - assert_eq!(problem.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6; 4] + ); + assert_eq!(problem.num_variables().unwrap(), 4); // Contacts get normalized so the smaller endpoint comes first. let contacts_2 = problem.contacts_2(); assert!(contacts_2.contains(&(0, 2))); diff --git a/src/unit_tests/models/graph/maximum_domatic_number.rs b/src/unit_tests/models/graph/maximum_domatic_number.rs index f922d8cea..e4984337d 100644 --- a/src/unit_tests/models/graph/maximum_domatic_number.rs +++ b/src/unit_tests/models/graph/maximum_domatic_number.rs @@ -7,12 +7,15 @@ use crate::types::Max; #[test] fn test_maximum_domatic_number_creation() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = MaximumDomaticNumber::new(graph); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![4; 4]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 4] + ); } #[test] @@ -32,7 +35,8 @@ fn test_maximum_domatic_number_evaluate_optimal() { (3, 5), (4, 5), ], - ); + ) + .unwrap(); let problem = MaximumDomaticNumber::new(graph); let config = vec![0, 1, 2, 0, 2, 1]; let result = problem.evaluate(&config).unwrap(); @@ -44,7 +48,7 @@ fn test_maximum_domatic_number_evaluate_invalid() { // Path graph P3: 0-1-2 // Config [0, 1, 2]: set {0} = {v0}, set {1} = {v1}, set {2} = {v2} // Set {2} = {v2} does NOT dominate v0 (v0 not in set and not adjacent to v2) - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MaximumDomaticNumber::new(graph); let config = vec![0, 1, 2]; let result = problem.evaluate(&config).unwrap(); @@ -54,7 +58,7 @@ fn test_maximum_domatic_number_evaluate_invalid() { #[test] fn test_maximum_domatic_number_evaluate_trivial() { // All vertices in one set → always a dominating set → Max(1) - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = MaximumDomaticNumber::new(graph); let config = vec![0, 0, 0, 0]; let result = problem.evaluate(&config).unwrap(); @@ -65,7 +69,7 @@ fn test_maximum_domatic_number_evaluate_trivial() { fn test_maximum_domatic_number_solver_p3() { // Path graph P3: 0-1-2 // Domatic number = 2: e.g., {0,2} and {1} are both dominating sets - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MaximumDomaticNumber::new(graph); let solver = BruteForce::new(); let witness = solver.solve(&problem).unwrap().unwrap(); @@ -76,7 +80,7 @@ fn test_maximum_domatic_number_solver_p3() { #[test] fn test_maximum_domatic_number_solver_complete_graph() { // K4: domatic number = 4 (each vertex is its own dominating set in K4) - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); let problem = MaximumDomaticNumber::new(graph); let solver = BruteForce::new(); let witness = solver.solve(&problem).unwrap().unwrap(); @@ -86,7 +90,7 @@ fn test_maximum_domatic_number_solver_complete_graph() { #[test] fn test_maximum_domatic_number_serialization() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MaximumDomaticNumber::new(graph); let json = serde_json::to_string(&problem).unwrap(); let deserialized: MaximumDomaticNumber = serde_json::from_str(&json).unwrap(); @@ -96,7 +100,7 @@ fn test_maximum_domatic_number_serialization() { #[test] fn test_maximum_domatic_number_parameter_getters() { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(); let problem = MaximumDomaticNumber::new(graph); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 4); @@ -105,7 +109,7 @@ fn test_maximum_domatic_number_parameter_getters() { #[test] fn test_maximum_domatic_number_single_vertex() { // Single vertex: domatic number = 1 - let graph = SimpleGraph::new(1, vec![]); + let graph = SimpleGraph::new(1, vec![]).unwrap(); let problem = MaximumDomaticNumber::new(graph); let config = vec![0]; assert_eq!(problem.evaluate(&config).unwrap(), Max(Some(1))); diff --git a/src/unit_tests/models/graph/maximum_edge_weighted_k_clique.rs b/src/unit_tests/models/graph/maximum_edge_weighted_k_clique.rs index a00cd688c..986bd4a9f 100644 --- a/src/unit_tests/models/graph/maximum_edge_weighted_k_clique.rs +++ b/src/unit_tests/models/graph/maximum_edge_weighted_k_clique.rs @@ -4,7 +4,7 @@ use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_edge_weights() { let p = MaximumEdgeWeightedKClique::try_from(MaximumEdgeWeightedKCliqueCreateSpec:: { - graph: SimpleGraph::new(2, vec![(0, 1)]), + graph: SimpleGraph::new(2, vec![(0, 1)]).unwrap(), edge_weights: None, k: 2, }) @@ -21,7 +21,7 @@ use crate::types::Max; /// and k = 3. Triangles are {0,1,2} (value 8) and {0,1,3} (value 6). fn issue_instance() -> MaximumEdgeWeightedKClique { MaximumEdgeWeightedKClique::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]).unwrap(), vec![5, 4, -1, 1, 0], 3, ) @@ -35,8 +35,11 @@ fn test_maximum_edge_weighted_k_clique_creation() { assert_eq!(problem.num_edges(), 5); assert_eq!(problem.k(), 3); assert_eq!(problem.edge_weights(), &[5, 4, -1, 1, 0]); - assert_eq!(problem.dimensions(), vec![2; 4]); - assert_eq!(problem.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); + assert_eq!(problem.num_variables().unwrap(), 4); assert!(problem.graph().has_edge(0, 1)); assert!(!problem.graph().has_edge(2, 3)); } @@ -123,7 +126,7 @@ fn test_maximum_edge_weighted_k_clique_k_zero_returns_zero() { // With k = 0 the unique feasible config selects no vertices and the // induced edge set is empty, so the objective is 0. let problem = MaximumEdgeWeightedKClique::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]).unwrap(), vec![5, 4, -1, 1, 0], 0, ) @@ -151,7 +154,7 @@ fn test_maximum_edge_weighted_k_clique_k_one_returns_zero() { // induced edge set is empty regardless of edge weights, so all feasible // configurations evaluate to 0. let problem = MaximumEdgeWeightedKClique::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]).unwrap(), vec![5, 4, -1, 1, 0], 1, ) @@ -181,7 +184,7 @@ fn test_maximum_edge_weighted_k_clique_k_one_returns_zero() { fn test_maximum_edge_weighted_k_clique_f64_variant() { // f64 variant exercises the additional registered weight type. let problem = MaximumEdgeWeightedKClique::::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]).unwrap(), vec![5.0, 4.0, -1.0, 1.0, 0.0], 3, ) @@ -227,7 +230,7 @@ fn test_maximum_edge_weighted_k_clique_problem_name_and_variant() { #[test] fn test_maximum_edge_weighted_k_clique_rejects_weight_length_mismatch() { let error = MaximumEdgeWeightedKClique::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]).unwrap(), vec![1, 2, 3, 4], // length 4 != 5 edges 3, ) @@ -242,7 +245,7 @@ fn test_maximum_edge_weighted_k_clique_rejects_weight_length_mismatch() { #[test] fn test_maximum_edge_weighted_k_clique_rejects_k_greater_than_n() { let error = MaximumEdgeWeightedKClique::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]).unwrap(), vec![5, 4, -1, 1, 0], 5, ) @@ -256,6 +259,6 @@ fn test_maximum_edge_weighted_k_clique_rejects_k_greater_than_n() { #[test] fn test_maximum_edge_weighted_k_clique_rejects_non_finite_weight() { - let graph = SimpleGraph::new(2, vec![(0, 1)]); + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); assert!(MaximumEdgeWeightedKClique::new(graph, vec![f64::NAN], 2).is_err()); } diff --git a/src/unit_tests/models/graph/maximum_independent_set.rs b/src/unit_tests/models/graph/maximum_independent_set.rs index e5fb725ca..961466b0f 100644 --- a/src/unit_tests/models/graph/maximum_independent_set.rs +++ b/src/unit_tests/models/graph/maximum_independent_set.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_simple_weights() { let problem = MaximumIndependentSet::try_from(MaximumIndependentSetSimpleI64CreateSpec { @@ -10,25 +9,35 @@ fn create_spec_defaults_simple_weights() { .unwrap(); assert_eq!(problem.weights(), &[1, 1, 1]); } +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; -include!("../../jl_helpers.rs"); #[test] fn test_independent_set_creation() { let problem = MaximumIndependentSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.dimensions().len(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 4 + ); } #[test] fn test_evaluate_reports_non_finite_weight_sum() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(2, vec![]), vec![f64::MAX, f64::MAX]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(2, vec![]).unwrap(), + vec![f64::MAX, f64::MAX], + ) + .unwrap(); assert!(matches!( problem.evaluate(&vec![true, true]), @@ -38,7 +47,8 @@ fn test_evaluate_reports_non_finite_weight_sum() { #[test] fn test_evaluate_rejects_invalid_configurations() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(2, vec![]), vec![1_i64, 1]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(2, vec![]).unwrap(), vec![1_i64, 1]).unwrap(); for solution in [vec![true], vec![true, false, false]] { assert!(matches!( problem.evaluate(&solution), @@ -53,7 +63,9 @@ fn test_evaluate_rejects_invalid_configurations() { #[test] fn test_independent_set_with_weights() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1, 2, 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1, 2, 3]) + .unwrap(); assert_eq!(problem.weights().to_vec(), vec![1, 2, 3]); assert!(problem.is_weighted()); } @@ -61,14 +73,19 @@ fn test_independent_set_with_weights() { #[test] fn test_independent_set_unweighted() { // i64 type is always considered weighted, even with uniform values - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); assert!(problem.is_weighted()); } #[test] fn test_has_edge() { - let problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); assert!(problem.graph().has_edge(0, 1)); assert!(problem.graph().has_edge(1, 0)); // Undirected assert!(problem.graph().has_edge(1, 2)); @@ -78,31 +95,34 @@ fn test_has_edge() { #[test] fn test_is_independent_set_function() { assert!(is_independent_set( - &SimpleGraph::new(3, vec![(0, 1)]), + &SimpleGraph::new(3, vec![(0, 1)]).unwrap(), &[true, false, true] )); assert!(is_independent_set( - &SimpleGraph::new(3, vec![(0, 1)]), + &SimpleGraph::new(3, vec![(0, 1)]).unwrap(), &[false, true, true] )); assert!(!is_independent_set( - &SimpleGraph::new(3, vec![(0, 1)]), + &SimpleGraph::new(3, vec![(0, 1)]).unwrap(), &[true, true, false] )); assert!(is_independent_set( - &SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), &[true, false, true] )); assert!(!is_independent_set( - &SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), &[false, true, true] )); } #[test] fn test_edges() { - let problem = - MaximumIndependentSet::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), vec![1i64; 4]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(), + vec![1i64; 4], + ) + .unwrap(); let edges = problem.graph().edges(); assert_eq!(edges.len(), 2); assert!(edges.contains(&(0, 1)) || edges.contains(&(1, 0))); @@ -111,29 +131,33 @@ fn test_edges() { #[test] fn test_with_custom_weights() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![5, 10, 15]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![5, 10, 15]) + .unwrap(); assert_eq!(problem.weights().to_vec(), vec![5, 10, 15]); } #[test] fn test_from_graph() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MaximumIndependentSet::new(graph.clone(), vec![1, 2, 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MaximumIndependentSet::new(graph.clone(), vec![1, 2, 3]).unwrap(); assert_eq!(problem.graph().num_vertices(), 3); assert_eq!(problem.weights().to_vec(), vec![1, 2, 3]); } #[test] fn test_from_graph_with_unit_weights() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MaximumIndependentSet::new(graph, vec![1i64; 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MaximumIndependentSet::new(graph, vec![1i64; 3]).unwrap(); assert_eq!(problem.graph().num_vertices(), 3); assert_eq!(problem.weights().to_vec(), vec![1, 1, 1]); } #[test] fn test_graph_accessor() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); let graph = problem.graph(); assert_eq!(graph.num_vertices(), 3); assert_eq!(graph.num_edges(), 1); @@ -141,7 +165,9 @@ fn test_graph_accessor() { #[test] fn test_weights() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![5, 10, 15]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![5, 10, 15]) + .unwrap(); assert_eq!(problem.weights(), &[5, 10, 15]); } @@ -163,7 +189,8 @@ fn test_jl_parity_evaluation() { let nv = instance["instance"]["num_vertices"].as_u64().unwrap() as usize; let edges = jl_parse_edges(&instance["instance"]); let weights = jl_parse_i64_vec(&instance["instance"]["weights"]); - let problem = MaximumIndependentSet::new(SimpleGraph::new(nv, edges), weights); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(nv, edges).unwrap(), weights).unwrap(); for eval in instance["evaluations"].as_array().unwrap() { let config = jl_parse_bool_config(&eval["config"]); let result = problem.evaluate(&config).unwrap(); @@ -194,8 +221,11 @@ fn test_jl_parity_evaluation() { #[test] fn test_is_valid_solution() { // Path graph: 0-1-2 - let problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); // Valid: {0, 2} is independent assert!(problem.is_valid_solution(&[true, false, true])); // Invalid: {0, 1} are adjacent @@ -205,9 +235,10 @@ fn test_is_valid_solution() { #[test] fn test_parameter_getters() { let problem = MaximumIndependentSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); } @@ -234,8 +265,9 @@ fn test_mis_paper_example() { (3, 8), (4, 9), // spokes ], - ); - let problem = MaximumIndependentSet::new(graph, vec![1i64; 10]); + ) + .unwrap(); + let problem = MaximumIndependentSet::new(graph, vec![1i64; 10]).unwrap(); // MIS = {1,3,5,9} -> config let config = vec![ false, true, false, true, false, true, false, false, false, true, @@ -249,3 +281,18 @@ fn test_mis_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap().unwrap(), 4); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::new(1, vec![]).unwrap(); + assert!(MaximumIndependentSet::new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "weights": []}); + assert!( + serde_json::from_value::>(json.clone()).is_err() + ); + let variant = std::collections::BTreeMap::from([ + ("graph".into(), "SimpleGraph".into()), + ("weight".into(), "i64".into()), + ]); + assert!(crate::registry::load_dyn("MaximumIndependentSet", &variant, json).is_err()); +} diff --git a/src/unit_tests/models/graph/maximum_leaf_spanning_tree.rs b/src/unit_tests/models/graph/maximum_leaf_spanning_tree.rs index 4698599ce..2f2c578e5 100644 --- a/src/unit_tests/models/graph/maximum_leaf_spanning_tree.rs +++ b/src/unit_tests/models/graph/maximum_leaf_spanning_tree.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; use crate::{solvers::BruteForce, topology::SimpleGraph, traits::Problem}; /// Issue #897 example: 6 vertices, 9 edges. @@ -18,8 +17,9 @@ fn example_instance() -> MaximumLeafSpanningTree { (4, 5), (1, 3), ], - ); - MaximumLeafSpanningTree::new(graph) + ) + .unwrap(); + MaximumLeafSpanningTree::new(graph).unwrap() } #[test] @@ -29,15 +29,22 @@ fn test_maximum_leaf_spanning_tree_creation() { assert_eq!(problem.graph().num_edges(), 9); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 9); - assert_eq!(problem.dimensions().len(), 9); - assert!(problem.dimensions().iter().all(|&d| d == 2)); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 9 + ); + assert!(crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .iter() + .all(|&d| d == 2)); } #[test] -#[should_panic(expected = "graph must have at least 2 vertices")] fn test_maximum_leaf_spanning_tree_rejects_tiny_graph() { - let graph = SimpleGraph::new(1, vec![]); - let _ = MaximumLeafSpanningTree::new(graph); + let graph = SimpleGraph::new(1, vec![]).unwrap(); + assert!(MaximumLeafSpanningTree::new(graph).is_err()); } #[test] @@ -126,9 +133,12 @@ fn test_maximum_leaf_spanning_tree_serialization() { #[test] fn test_maximum_leaf_spanning_tree_small_path() { // Path graph P3: 0-1-2, only spanning tree is the path itself -> 2 leaves - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MaximumLeafSpanningTree::new(graph); - assert_eq!(problem.dimensions(), vec![2, 2]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MaximumLeafSpanningTree::new(graph).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2] + ); let config = vec![true, true]; assert_eq!(problem.evaluate(&config).unwrap(), Max(Some(2))); } @@ -137,8 +147,8 @@ fn test_maximum_leaf_spanning_tree_small_path() { fn test_maximum_leaf_spanning_tree_star() { // Star K1,3: center 0, leaves 1,2,3 // Edges: (0,1),(0,2),(0,3) - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); - let problem = MaximumLeafSpanningTree::new(graph); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(); + let problem = MaximumLeafSpanningTree::new(graph).unwrap(); // Only one spanning tree: all 3 edges let config = vec![true, true, true]; assert_eq!(problem.evaluate(&config).unwrap(), Max(Some(3))); diff --git a/src/unit_tests/models/graph/maximum_matching.rs b/src/unit_tests/models/graph/maximum_matching.rs index d39e31e4c..bc3267baa 100644 --- a/src/unit_tests/models/graph/maximum_matching.rs +++ b/src/unit_tests/models/graph/maximum_matching.rs @@ -1,32 +1,37 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Max; -include!("../../jl_helpers.rs"); #[test] fn test_matching_creation() { let problem = MaximumMatching::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1, 2, 3], - ); + ) + .unwrap(); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.num_variables(), 3); + assert_eq!(problem.num_variables().unwrap(), 3); } #[test] fn test_matching_unit_weights() { let problem = - MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); assert_eq!(problem.graph().num_edges(), 2); } #[test] fn test_edge_endpoints() { - let problem = MaximumMatching::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 2]); + let problem = MaximumMatching::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1, 2], + ) + .unwrap(); assert_eq!(problem.edge_endpoints(0), Some((0, 1))); assert_eq!(problem.edge_endpoints(1), Some((1, 2))); assert_eq!(problem.edge_endpoints(2), None); @@ -35,9 +40,10 @@ fn test_edge_endpoints() { #[test] fn test_is_valid_matching() { let problem = MaximumMatching::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1, 1, 1], - ); + ) + .unwrap(); // Valid: select edge 0 only assert!(problem.is_valid_matching(&[true, false, false])); @@ -51,7 +57,7 @@ fn test_is_valid_matching() { #[test] fn test_is_matching_function() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); assert!(is_matching(&graph, &[true, false, true])); // Disjoint assert!(is_matching(&graph, &[false, true, false])); // Single edge @@ -61,21 +67,25 @@ fn test_is_matching_function() { #[test] fn test_empty_graph() { - let problem = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![])); + let problem = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![]).unwrap()); // Empty matching is valid with size 0 assert_eq!(Problem::evaluate(&problem, &vec![]).unwrap(), Max(Some(0))); } #[test] fn test_edges() { - let problem = MaximumMatching::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![5, 10]); + let problem = MaximumMatching::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![5, 10], + ) + .unwrap(); let edges = problem.edges(); assert_eq!(edges.len(), 2); } #[test] fn test_empty_sets() { - let problem = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(2, vec![])); + let problem = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(2, vec![]).unwrap()); // Empty matching assert_eq!(Problem::evaluate(&problem, &vec![]).unwrap(), Max(Some(0))); } @@ -83,13 +93,17 @@ fn test_empty_sets() { #[test] #[should_panic(expected = "selected length must match num_edges")] fn test_is_matching_wrong_len() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); is_matching(&graph, &[true]); // Wrong length } #[test] fn test_new() { - let problem = MaximumMatching::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![5, 10]); + let problem = MaximumMatching::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![5, 10], + ) + .unwrap(); assert_eq!(problem.graph().num_vertices(), 3); assert_eq!(problem.graph().num_edges(), 2); assert_eq!(problem.weights(), vec![5, 10]); @@ -98,7 +112,7 @@ fn test_new() { #[test] fn test_unit_weights() { let problem = - MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); assert_eq!(problem.graph().num_vertices(), 3); assert_eq!(problem.graph().num_edges(), 2); assert_eq!(problem.weights(), vec![1, 1]); @@ -107,7 +121,7 @@ fn test_unit_weights() { #[test] fn test_graph_accessor() { let problem = - MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); assert_eq!(problem.graph().num_vertices(), 3); assert_eq!(problem.graph().num_edges(), 2); } @@ -121,7 +135,7 @@ fn test_jl_parity_evaluation() { let weighted_edges = jl_parse_weighted_edges(&instance["instance"]); let edges: Vec<(usize, usize)> = weighted_edges.iter().map(|&(u, v, _)| (u, v)).collect(); let weights: Vec = weighted_edges.into_iter().map(|(_, _, w)| w).collect(); - let problem = MaximumMatching::new(SimpleGraph::new(nv, edges), weights); + let problem = MaximumMatching::new(SimpleGraph::new(nv, edges).unwrap(), weights).unwrap(); for eval in instance["evaluations"].as_array().unwrap() { let config = jl_parse_bool_config(&eval["config"]); let result = problem.evaluate(&config).unwrap(); @@ -153,9 +167,10 @@ fn test_jl_parity_evaluation() { fn test_is_valid_solution() { // Triangle: edges (0,1), (1,2), (0,2) — config is per edge let problem = MaximumMatching::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); // Valid: select edge (0,1) only — no shared vertices assert!(problem.is_valid_solution(&[true, false, false])); // Invalid: select edges (0,1) and (1,2) — vertex 1 shared @@ -165,9 +180,10 @@ fn test_is_valid_solution() { #[test] fn test_parameter_getters() { let problem = MaximumMatching::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); } @@ -175,7 +191,7 @@ fn test_parameter_getters() { #[test] fn test_matching_paper_example() { // Paper: house graph, M = {(v_0,v_1), (v_2,v_4)}, weight = 2 - let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(); let problem = MaximumMatching::<_, i64>::unit_weights(graph); // Edges: 0=(0,1), 1=(0,2), 2=(1,3), 3=(2,3), 4=(2,4), 5=(3,4) // Select edges 0 and 4 @@ -199,3 +215,21 @@ fn create_spec_uses_edge_weights_and_defaults_to_one() { assert_eq!(problem.weights(), vec![1]); assert_eq!(MaximumMatchingCreateSpec::FIELDS[2].name, "edge_weights"); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); + assert!(MaximumMatching::new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "edge_weights": []}); + assert!(serde_json::from_value::>(json).is_err()); +} + +#[test] +fn rejected_weight_update_preserves_instance() { + let mut problem = + MaximumMatching::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![3i64; 1]).unwrap(); + let before = serde_json::to_value(&problem).unwrap(); + assert!(problem.set_weights(vec![]).is_err()); + assert_eq!(serde_json::to_value(&problem).unwrap(), before); + problem.set_weights(vec![4; 1]).unwrap(); +} diff --git a/src/unit_tests/models/graph/min_max_multicenter.rs b/src/unit_tests/models/graph/min_max_multicenter.rs index 8db1f88fa..cb12514a8 100644 --- a/src/unit_tests/models/graph/min_max_multicenter.rs +++ b/src/unit_tests/models/graph/min_max_multicenter.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Min; @@ -12,8 +11,9 @@ fn example_instance() -> MinMaxMulticenter { let graph = SimpleGraph::new( 6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 5), (1, 4)], - ); - MinMaxMulticenter::new(graph, vec![1i64; 6], vec![1i64; 7], 2) + ) + .unwrap(); + MinMaxMulticenter::new(graph, vec![1i64; 6], vec![1i64; 7], 2).unwrap() } #[test] @@ -24,7 +24,10 @@ fn test_minmaxmulticenter_basic() { assert_eq!(problem.k(), 2); assert_eq!(problem.vertex_weights(), &[1, 1, 1, 1, 1, 1]); assert_eq!(problem.edge_lengths(), &[1, 1, 1, 1, 1, 1, 1]); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 7); assert_eq!(problem.num_centers(), 2); @@ -127,8 +130,8 @@ fn test_minmaxmulticenter_solver() { #[test] fn test_minmaxmulticenter_disconnected() { // Two disconnected components: 0-1 and 2-3, K=1 - let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]); - let problem = MinMaxMulticenter::new(graph, vec![1i64; 4], vec![1i64; 2], 1); + let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(); + let problem = MinMaxMulticenter::new(graph, vec![1i64; 4], vec![1i64; 2], 1).unwrap(); // Center at 0: vertices 2 and 3 are unreachable -> None assert_eq!( @@ -137,8 +140,8 @@ fn test_minmaxmulticenter_disconnected() { ); // With K=2, centers at {0, 2}: all reachable, max distance = 1 - let graph2 = SimpleGraph::new(4, vec![(0, 1), (2, 3)]); - let problem2 = MinMaxMulticenter::new(graph2, vec![1i64; 4], vec![1i64; 2], 2); + let graph2 = SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(); + let problem2 = MinMaxMulticenter::new(graph2, vec![1i64; 4], vec![1i64; 2], 2).unwrap(); assert_eq!( problem2.evaluate(&vec![true, false, true, false]).unwrap(), Min(Some(1)) @@ -148,8 +151,8 @@ fn test_minmaxmulticenter_disconnected() { #[test] fn test_minmaxmulticenter_weighted() { // Path: 0-1-2, vertex weights = [3, 1, 2], edge lengths = [1, 1], K=1 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinMaxMulticenter::new(graph, vec![3i64, 1, 2], vec![1i64; 2], 1); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinMaxMulticenter::new(graph, vec![3i64, 1, 2], vec![1i64; 2], 1).unwrap(); // Center at 1: d(0)=1, d(1)=0, d(2)=1 // w(0)*d(0) = 3*1 = 3, w(1)*d(1) = 0, w(2)*d(2) = 2*1 = 2 @@ -170,8 +173,8 @@ fn test_minmaxmulticenter_weighted() { #[test] fn test_minmaxmulticenter_single_vertex() { - let graph = SimpleGraph::new(1, vec![]); - let problem = MinMaxMulticenter::new(graph, vec![5i64], vec![], 1); + let graph = SimpleGraph::new(1, vec![]).unwrap(); + let problem = MinMaxMulticenter::new(graph, vec![5i64], vec![], 1).unwrap(); // Only vertex is the center, max weighted distance = 0 assert_eq!(problem.evaluate(&vec![true]).unwrap(), Min(Some(0))); } @@ -179,8 +182,8 @@ fn test_minmaxmulticenter_single_vertex() { #[test] fn test_minmaxmulticenter_all_centers() { // K = num_vertices: all vertices are centers, max distance = 0 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinMaxMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 3); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinMaxMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 3).unwrap(); assert_eq!( problem.evaluate(&vec![true, true, true]).unwrap(), Min(Some(0)) @@ -190,8 +193,8 @@ fn test_minmaxmulticenter_all_centers() { #[test] fn test_minmaxmulticenter_nonunit_edge_lengths() { // Path: 0-1-2, unit vertex weights, edge lengths [1, 3], K=1 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinMaxMulticenter::new(graph, vec![1i64; 3], vec![1i64, 3], 1); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinMaxMulticenter::new(graph, vec![1i64; 3], vec![1i64, 3], 1).unwrap(); // Center at 0: d(0)=0, d(1)=1, d(2)=1+3=4; max=4 assert_eq!( @@ -213,45 +216,39 @@ fn test_minmaxmulticenter_nonunit_edge_lengths() { } #[test] -#[should_panic(expected = "vertex_weights length must match num_vertices")] fn test_minmaxmulticenter_wrong_vertex_weights_len() { - let graph = SimpleGraph::new(3, vec![(0, 1)]); - MinMaxMulticenter::new(graph, vec![1i64; 2], vec![1i64; 1], 1); + let graph = SimpleGraph::new(3, vec![(0, 1)]).unwrap(); + assert!(MinMaxMulticenter::new(graph, vec![1i64; 2], vec![1i64; 1], 1).is_err()); } #[test] -#[should_panic(expected = "edge_lengths length must match num_edges")] fn test_minmaxmulticenter_wrong_edge_lengths_len() { - let graph = SimpleGraph::new(3, vec![(0, 1)]); - MinMaxMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 1); + let graph = SimpleGraph::new(3, vec![(0, 1)]).unwrap(); + assert!(MinMaxMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 1).is_err()); } #[test] -#[should_panic(expected = "k must be positive")] fn test_minmaxmulticenter_k_zero() { - let graph = SimpleGraph::new(3, vec![(0, 1)]); - MinMaxMulticenter::new(graph, vec![1i64; 3], vec![1i64; 1], 0); + let graph = SimpleGraph::new(3, vec![(0, 1)]).unwrap(); + assert!(MinMaxMulticenter::new(graph, vec![1i64; 3], vec![1i64; 1], 0).is_err()); } #[test] -#[should_panic(expected = "k must not exceed num_vertices")] fn test_minmaxmulticenter_k_too_large() { - let graph = SimpleGraph::new(3, vec![(0, 1)]); - MinMaxMulticenter::new(graph, vec![1i64; 3], vec![1i64; 1], 4); + let graph = SimpleGraph::new(3, vec![(0, 1)]).unwrap(); + assert!(MinMaxMulticenter::new(graph, vec![1i64; 3], vec![1i64; 1], 4).is_err()); } #[test] -#[should_panic(expected = "vertex_weights must be non-negative")] fn test_minmaxmulticenter_negative_vertex_weight() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - MinMaxMulticenter::new(graph, vec![1i64, -1, 1], vec![1i64; 2], 1); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + assert!(MinMaxMulticenter::new(graph, vec![1i64, -1, 1], vec![1i64; 2], 1).is_err()); } #[test] -#[should_panic(expected = "edge_lengths must be non-negative")] fn test_minmaxmulticenter_negative_edge_length() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - MinMaxMulticenter::new(graph, vec![1i64; 3], vec![1i64, -1], 1); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + assert!(MinMaxMulticenter::new(graph, vec![1i64; 3], vec![1i64, -1], 1).is_err()); } #[test] fn create_specs_map_weight_inputs_for_both_variants() { diff --git a/src/unit_tests/models/graph/minimum_capacitated_spanning_tree.rs b/src/unit_tests/models/graph/minimum_capacitated_spanning_tree.rs index 3576d79b5..d5ba8e13a 100644 --- a/src/unit_tests/models/graph/minimum_capacitated_spanning_tree.rs +++ b/src/unit_tests/models/graph/minimum_capacitated_spanning_tree.rs @@ -1,10 +1,9 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_edge_weights() { let p = MinimumCapacitatedSpanningTree::try_from(MinimumCapacitatedSpanningTreeCreateSpec { - graph: SimpleGraph::new(2, vec![(0, 1)]), + graph: SimpleGraph::new(2, vec![(0, 1)]).unwrap(), weights: None, root: 0, requirements: vec![0, 1], @@ -31,20 +30,21 @@ fn example_instance() -> MinimumCapacitatedSpanningTree { (2, 4), (3, 4), ], - ); + ) + .unwrap(); let weights = vec![2, 1, 4, 3, 1, 2, 3, 1]; let requirements = vec![0, 1, 1, 1, 1]; let capacity = 3; - MinimumCapacitatedSpanningTree::new(graph, weights, 0, requirements, capacity) + MinimumCapacitatedSpanningTree::new(graph, weights, 0, requirements, capacity).unwrap() } /// Tight capacity instance: capacity=2, so each subtree can hold at most 2 vertices. fn tight_capacity_instance() -> MinimumCapacitatedSpanningTree { - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (2, 3)]).unwrap(); let weights = vec![1, 2, 3, 1, 1]; let requirements = vec![0, 1, 1, 1]; let capacity = 2; - MinimumCapacitatedSpanningTree::new(graph, weights, 0, requirements, capacity) + MinimumCapacitatedSpanningTree::new(graph, weights, 0, requirements, capacity).unwrap() } #[test] @@ -55,36 +55,42 @@ fn test_creation() { assert_eq!(problem.root(), 0); assert_eq!(problem.requirements(), &[0, 1, 1, 1, 1]); assert_eq!(*problem.capacity(), 3); - assert_eq!(problem.dimensions().len(), 8); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 8 + ); assert!(problem.is_weighted()); } #[test] -#[should_panic(expected = "weights length must match num_edges")] fn test_rejects_wrong_weight_count() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let _ = MinimumCapacitatedSpanningTree::new(graph, vec![1, 1, 1], 0, vec![0, 1, 1], 3); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + assert!( + MinimumCapacitatedSpanningTree::new(graph, vec![1, 1, 1], 0, vec![0, 1, 1], 3).is_err() + ); } #[test] -#[should_panic(expected = "requirements length must match num_vertices")] fn test_rejects_wrong_requirements_count() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let _ = MinimumCapacitatedSpanningTree::new(graph, vec![1, 1], 0, vec![0, 1], 3); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + assert!(MinimumCapacitatedSpanningTree::new(graph, vec![1, 1], 0, vec![0, 1], 3).is_err()); } #[test] -#[should_panic(expected = "root 5 out of range")] fn test_rejects_invalid_root() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let _ = MinimumCapacitatedSpanningTree::new(graph, vec![1, 1], 5, vec![0, 1, 1], 3); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + assert!(MinimumCapacitatedSpanningTree::new(graph, vec![1, 1], 5, vec![0, 1, 1], 3).is_err()); } #[test] -#[should_panic(expected = "graph must have at least 2 vertices")] fn test_rejects_single_vertex() { - let graph = SimpleGraph::new(1, vec![]); - let _ = MinimumCapacitatedSpanningTree::::new(graph, vec![], 0, vec![0], 3); + let graph = SimpleGraph::new(1, vec![]).unwrap(); + assert!( + MinimumCapacitatedSpanningTree::::new(graph, vec![], 0, vec![0], 3) + .is_err() + ); } #[test] @@ -184,7 +190,7 @@ fn test_parameter_getters() { fn test_set_weights() { let mut problem = example_instance(); assert_eq!(problem.weights(), &[2, 1, 4, 3, 1, 2, 3, 1]); - problem.set_weights(vec![1; 8]); + problem.set_weights(vec![1; 8]).unwrap(); assert_eq!(problem.weights(), &[1; 8]); // Same optimal tree now has cost 4 let config = vec![true, true, false, false, true, false, false, true]; diff --git a/src/unit_tests/models/graph/minimum_cost_circulation.rs b/src/unit_tests/models/graph/minimum_cost_circulation.rs index c22563730..18160d60b 100644 --- a/src/unit_tests/models/graph/minimum_cost_circulation.rs +++ b/src/unit_tests/models/graph/minimum_cost_circulation.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::DirectedGraph; use crate::traits::Problem; use crate::types::Min; @@ -19,10 +18,11 @@ use crate::types::Min; /// cost = 2*2 + 2*(-3) + 1*1 + 1*(-4) = 4 - 6 + 1 - 4 = -5 fn canonical_instance() -> MinimumCostCirculation { MinimumCostCirculation::new( - DirectedGraph::new(3, vec![(0, 1), (1, 0), (0, 2), (2, 0)]), + DirectedGraph::new(3, vec![(0, 1), (1, 0), (0, 2), (2, 0)]).unwrap(), vec![2, 2, 1, 1], vec![2, -3, 1, -4], ) + .unwrap() } #[test] @@ -32,7 +32,10 @@ fn test_minimum_cost_circulation_creation() { assert_eq!(problem.num_arcs(), 4); assert_eq!(problem.capacities(), &[2, 2, 1, 1]); assert_eq!(problem.costs(), &[2, -3, 1, -4]); - assert_eq!(problem.dimensions(), vec![3, 3, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3, 2, 2] + ); assert_eq!( ::NAME, "MinimumCostCirculation" @@ -129,10 +132,11 @@ fn test_minimum_cost_circulation_negative_cycle_beats_zero() { // trivial zero circulation. Graph is one cycle 0 -> 1 -> 0 with // per-unit cost 1 + (-3) = -2, capacity 1. let problem = MinimumCostCirculation::new( - DirectedGraph::new(2, vec![(0, 1), (1, 0)]), + DirectedGraph::new(2, vec![(0, 1), (1, 0)]).unwrap(), vec![1, 1], vec![1, -3], - ); + ) + .unwrap(); let solver = BruteForce::new(); let witness = solver .solve(&problem) @@ -152,10 +156,11 @@ fn test_minimum_cost_circulation_issue_example_1030() { // Bottleneck is the back-arc (cap=1), so the optimum sends one unit // around the cycle: cost = 1*3 + 1*(-5) = -2. let problem = MinimumCostCirculation::new( - DirectedGraph::new(2, vec![(0, 1), (1, 0)]), + DirectedGraph::new(2, vec![(0, 1), (1, 0)]).unwrap(), vec![2, 1], vec![3, -5], - ); + ) + .unwrap(); let solver = BruteForce::new(); let witness = solver .solve(&problem) diff --git a/src/unit_tests/models/graph/minimum_cost_maximum_flow.rs b/src/unit_tests/models/graph/minimum_cost_maximum_flow.rs index 5c56ee644..866c85262 100644 --- a/src/unit_tests/models/graph/minimum_cost_maximum_flow.rs +++ b/src/unit_tests/models/graph/minimum_cost_maximum_flow.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::DirectedGraph; use crate::traits::Problem; use crate::types::Min; @@ -18,7 +17,7 @@ use crate::types::Min; /// Optimal config = [2, 1, 1, 1, 2] with cost = 2*1 + 0 + 0 + 1 + 2*2 = 7. fn canonical_instance() -> MinimumCostMaximumFlow { MinimumCostMaximumFlow::new( - DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]), + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]).unwrap(), 0, 3, vec![2, 1, 1, 1, 2], @@ -41,7 +40,7 @@ fn canonical_instance() -> MinimumCostMaximumFlow { /// 0->1->3->4 has cost 5. Brute force must pick the cheaper route. fn lex_tiebreaker_instance() -> MinimumCostMaximumFlow { MinimumCostMaximumFlow::new( - DirectedGraph::new(5, vec![(0, 1), (1, 2), (1, 3), (2, 4), (3, 4)]), + DirectedGraph::new(5, vec![(0, 1), (1, 2), (1, 3), (2, 4), (3, 4)]).unwrap(), 0, 4, vec![1, 1, 1, 1, 1], @@ -58,7 +57,10 @@ fn test_minimum_cost_maximum_flow_creation() { assert_eq!(problem.sink(), 3); assert_eq!(problem.capacities(), &[2, 1, 1, 1, 2]); assert_eq!(problem.costs(), &[1, 0, 0, 1, 2]); - assert_eq!(problem.dimensions(), vec![3, 2, 2, 2, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 2, 2, 2, 3] + ); assert_eq!( ::NAME, "MinimumCostMaximumFlow" diff --git a/src/unit_tests/models/graph/minimum_covering_by_cliques.rs b/src/unit_tests/models/graph/minimum_covering_by_cliques.rs index e2916b241..0eb8f2835 100644 --- a/src/unit_tests/models/graph/minimum_covering_by_cliques.rs +++ b/src/unit_tests/models/graph/minimum_covering_by_cliques.rs @@ -7,19 +7,22 @@ use crate::types::Min; #[test] fn test_minimum_covering_by_cliques_creation() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = MinimumCoveringByCliques::new(graph); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); - assert_eq!(problem.num_variables(), 3); + assert_eq!(problem.num_variables().unwrap(), 3); // Each edge can be assigned to one of 3 groups - assert_eq!(problem.dimensions(), vec![3; 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 3] + ); } #[test] fn test_minimum_covering_by_cliques_triangle() { // Triangle: all 3 edges form a single clique -> 1 group suffices - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); let problem = MinimumCoveringByCliques::new(graph); // All edges in group 0 -> valid, 1 clique @@ -35,7 +38,7 @@ fn test_minimum_covering_by_cliques_triangle() { fn test_minimum_covering_by_cliques_path() { // Path 0-1-2: edges (0,1) and (1,2) are each individual cliques (K2) // but cannot be combined into one clique since 0 and 2 are not adjacent. - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MinimumCoveringByCliques::new(graph); // Both edges in the same group -> invalid (0 and 2 not adjacent) @@ -53,7 +56,7 @@ fn test_minimum_covering_by_cliques_path() { fn test_minimum_covering_by_cliques_invalid_group() { // Square: 0-1-2-3-0, edges (0,1),(1,2),(2,3),(3,0) // Putting non-adjacent-endpoint edges in same group is invalid - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(); let problem = MinimumCoveringByCliques::new(graph); // Edges (0,1) and (2,3) in same group: vertices {0,1,2,3}, not a clique @@ -66,14 +69,14 @@ fn test_minimum_covering_by_cliques_invalid_group() { #[test] fn test_minimum_covering_by_cliques_empty_graph() { // No edges: 0 cliques needed - let graph = SimpleGraph::new(3, vec![]); + let graph = SimpleGraph::new(3, vec![]).unwrap(); let problem = MinimumCoveringByCliques::new(graph); assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } #[test] fn test_minimum_covering_by_cliques_wrong_length() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MinimumCoveringByCliques::new(graph); assert!(matches!( problem.evaluate(&vec![0]), @@ -85,7 +88,7 @@ fn test_minimum_covering_by_cliques_wrong_length() { fn test_minimum_covering_by_cliques_solver() { // K4 minus one edge: 4 vertices, 5 edges // 0-1, 0-2, 0-3, 1-2, 2-3 (missing 1-3) - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (2, 3)]).unwrap(); let problem = MinimumCoveringByCliques::new(graph); let solver = BruteForce::new(); @@ -97,7 +100,7 @@ fn test_minimum_covering_by_cliques_solver() { #[test] fn test_minimum_covering_by_cliques_is_valid_cover() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); let problem = MinimumCoveringByCliques::new(graph); // All in one group (triangle) -> valid @@ -125,7 +128,8 @@ fn test_minimum_covering_by_cliques_paper_example() { (5, 2), (5, 3), ], - ); + ) + .unwrap(); let problem = MinimumCoveringByCliques::new(graph); // The given optimal config 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 84f6d6aab..83fb82d9f 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,10 +1,9 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_edge_weights() { let p = MinimumCutIntoBoundedSets::try_from(MinimumCutIntoBoundedSetsCreateSpec { - graph: SimpleGraph::new(2, vec![(0, 1)]), + graph: SimpleGraph::new(2, vec![(0, 1)]).unwrap(), edge_weights: None, source: 0, sink: 1, @@ -16,7 +15,7 @@ fn create_spec_defaults_edge_weights() { use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::{Min, SolutionAggregate}; +use crate::types::Min; /// Build the example instance from issue #228: /// 8 vertices, 12 edges, s=0, t=7, B=5 @@ -37,9 +36,10 @@ fn example_instance() -> MinimumCutIntoBoundedSets { (6, 7), (5, 6), ], - ); + ) + .unwrap(); let edge_weights = vec![2, 3, 1, 4, 2, 1, 3, 2, 1, 2, 3, 1]; - MinimumCutIntoBoundedSets::new(graph, edge_weights, 0, 7, 5) + MinimumCutIntoBoundedSets::new(graph, edge_weights, 0, 7, 5).unwrap() } #[test] @@ -50,7 +50,10 @@ fn test_minimumcutintoboundedsets_basic() { assert_eq!(problem.source(), 0); assert_eq!(problem.sink(), 7); assert_eq!(problem.size_bound(), 5); - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); } #[test] @@ -106,9 +109,10 @@ fn test_minimumcutintoboundedsets_size_bound_violated() { (6, 7), (5, 6), ], - ); + ) + .unwrap(); let edge_weights = vec![2, 3, 1, 4, 2, 1, 3, 2, 1, 2, 3, 1]; - let problem = MinimumCutIntoBoundedSets::new(graph, edge_weights, 0, 7, 3); + let problem = MinimumCutIntoBoundedSets::new(graph, edge_weights, 0, 7, 3).unwrap(); // V1={0,1,2,3} has 4 > B=3 let config = vec![false, false, false, false, true, true, true, true]; assert_eq!(problem.evaluate(&config).unwrap(), Min(None)); @@ -156,8 +160,8 @@ fn test_minimumcutintoboundedsets_solver() { #[test] fn test_minimumcutintoboundedsets_small_graph() { // Simple 3-vertex path: 0-1-2, s=0, t=2, B=2 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumCutIntoBoundedSets::new(graph, vec![1, 1], 0, 2, 2); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumCutIntoBoundedSets::new(graph, vec![1, 1], 0, 2, 2).unwrap(); // V1={0,1}, V2={2}: cut edge (1,2)=1 assert_eq!( problem.evaluate(&vec![false, false, true]).unwrap(), @@ -192,9 +196,3 @@ fn test_minimumcutintoboundedsets_variant() { assert!(variant.iter().any(|(k, _)| *k == "graph")); assert!(variant.iter().any(|(k, _)| *k == "weight")); } - -#[test] -fn test_minimumcutintoboundedsets_selects_optimal_solutions() { - type Value = as Problem>::Value; - assert!(Value::contributes_to_solution(&Min(Some(3)), &Min(Some(3)))); -} diff --git a/src/unit_tests/models/graph/minimum_dominating_set.rs b/src/unit_tests/models/graph/minimum_dominating_set.rs index 5edd12d7b..e8f0cfb39 100644 --- a/src/unit_tests/models/graph/minimum_dominating_set.rs +++ b/src/unit_tests/models/graph/minimum_dominating_set.rs @@ -8,40 +8,47 @@ fn create_spec_rejects_weight_count_mismatch() { "weights" ); let result = MinimumDominatingSet::try_from(MinimumDominatingSetCreateSpec { - graph: SimpleGraph::new(2, vec![(0, 1)]), + graph: SimpleGraph::new(2, vec![(0, 1)]).unwrap(), weights: vec![1], }); assert!(result.is_err()); } +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; -include!("../../jl_helpers.rs"); #[test] fn test_dominating_set_creation() { let problem = MinimumDominatingSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); } #[test] fn test_dominating_set_with_weights() { - let problem = MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1, 2, 3]); + let problem = + MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1, 2, 3]) + .unwrap(); assert_eq!(problem.weights(), &[1, 2, 3]); } #[test] fn test_neighbors() { let problem = MinimumDominatingSet::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let nbrs = problem.neighbors(0); assert!(nbrs.contains(&1)); assert!(nbrs.contains(&2)); @@ -50,8 +57,11 @@ fn test_neighbors() { #[test] fn test_closed_neighborhood() { - let problem = - MinimumDominatingSet::new(SimpleGraph::new(4, vec![(0, 1), (0, 2)]), vec![1i64; 4]); + let problem = MinimumDominatingSet::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2)]).unwrap(), + vec![1i64; 4], + ) + .unwrap(); let cn = problem.closed_neighborhood(0); assert!(cn.contains(&0)); assert!(cn.contains(&1)); @@ -61,7 +71,7 @@ fn test_closed_neighborhood() { #[test] fn test_is_dominating_set_function() { - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(); // Center dominates all assert!(is_dominating_set(&graph, &[true, false, false, false])); @@ -76,7 +86,9 @@ fn test_is_dominating_set_function() { #[test] fn test_isolated_vertex() { // Isolated vertex must be in dominating set - let problem = MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -91,42 +103,52 @@ fn test_isolated_vertex() { #[test] #[should_panic(expected = "selected length must match num_vertices")] fn test_is_dominating_set_wrong_len() { - is_dominating_set(&SimpleGraph::new(3, vec![(0, 1)]), &[true, false]); + is_dominating_set(&SimpleGraph::new(3, vec![(0, 1)]).unwrap(), &[true, false]); } #[test] fn test_from_graph() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumDominatingSet::new(graph, vec![1, 2, 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumDominatingSet::new(graph, vec![1, 2, 3]).unwrap(); assert_eq!(problem.graph().num_vertices(), 3); assert_eq!(problem.weights(), &[1, 2, 3]); } #[test] fn test_graph_accessor() { - let problem = MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); assert_eq!(problem.graph().num_vertices(), 3); assert_eq!(problem.graph().num_edges(), 1); } #[test] fn test_weights() { - let problem = MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![5, 10, 15]); + let problem = + MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![5, 10, 15]) + .unwrap(); assert_eq!(problem.weights(), &[5, 10, 15]); } #[test] fn test_edges() { - let problem = - MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MinimumDominatingSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); let edges = problem.graph().edges(); assert_eq!(edges.len(), 2); } #[test] fn test_has_edge() { - let problem = - MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MinimumDominatingSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); assert!(problem.graph().has_edge(0, 1)); assert!(problem.graph().has_edge(1, 0)); // Undirected assert!(problem.graph().has_edge(1, 2)); @@ -140,7 +162,9 @@ fn test_jl_parity_evaluation() { for instance in data["instances"].as_array().unwrap() { let nv = instance["instance"]["num_vertices"].as_u64().unwrap() as usize; let edges = jl_parse_edges(&instance["instance"]); - let problem = MinimumDominatingSet::new(SimpleGraph::new(nv, edges), vec![1i64; nv]); + let problem = + MinimumDominatingSet::new(SimpleGraph::new(nv, edges).unwrap(), vec![1i64; nv]) + .unwrap(); for eval in instance["evaluations"].as_array().unwrap() { let config = jl_parse_bool_config(&eval["config"]); let result = problem.evaluate(&config).unwrap(); @@ -171,8 +195,11 @@ fn test_jl_parity_evaluation() { #[test] fn test_is_valid_solution() { // Path graph: 0-1-2 - let problem = - MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MinimumDominatingSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); // Valid: {1} dominates all vertices (0 and 2 are neighbors of 1) assert!(problem.is_valid_solution(&[false, true, false])); // Invalid: {0} doesn't dominate vertex 2 @@ -181,8 +208,11 @@ fn test_is_valid_solution() { #[test] fn test_parameter_getters() { - let problem = - MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MinimumDominatingSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_edges(), 2); } @@ -190,8 +220,8 @@ fn test_parameter_getters() { #[test] fn test_mds_paper_example() { // Paper: house graph, DS = {v_2, v_3}, weight = 2, gamma(G) = 2 - let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); - let problem = MinimumDominatingSet::new(graph, vec![1i64; 5]); + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(); + let problem = MinimumDominatingSet::new(graph, vec![1i64; 5]).unwrap(); let config = vec![false, false, true, true, false]; // {v_2, v_3} let result = problem.evaluate(&config).unwrap(); assert!(result.is_valid()); @@ -201,3 +231,18 @@ fn test_mds_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap().unwrap(), 2); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::new(1, vec![]).unwrap(); + assert!(MinimumDominatingSet::new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "weights": []}); + assert!( + serde_json::from_value::>(json.clone()).is_err() + ); + let variant = std::collections::BTreeMap::from([ + ("graph".into(), "SimpleGraph".into()), + ("weight".into(), "i64".into()), + ]); + assert!(crate::registry::load_dyn("MinimumDominatingSet", &variant, json).is_err()); +} 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 fed0eb787..b325b6b88 100644 --- a/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs +++ b/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_cycle() { @@ -18,7 +17,7 @@ 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)]) + DirectedGraph::new(6, vec![(0, 2), (0, 3), (1, 3), (1, 4), (2, 5)]).unwrap() } fn issue_problem() -> MinimumDummyActivitiesPert { @@ -43,14 +42,18 @@ fn test_minimum_dummy_activities_pert_creation() { let problem = issue_problem(); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_arcs(), 5); - assert_eq!(problem.dimensions(), vec![2; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); } #[test] fn test_minimum_dummy_activities_pert_rejects_cyclic_input() { - let err = - MinimumDummyActivitiesPert::try_new(DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)])) - .unwrap_err(); + let err = MinimumDummyActivitiesPert::try_new( + DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(), + ) + .unwrap_err(); assert!(err.to_string().contains("DAG")); } @@ -90,7 +93,7 @@ fn test_minimum_dummy_activities_pert_transitive_arc_zero_dummies() { // DAG with transitive arc: 0→1, 1→2, 0→2. // Merging 0+=1- and 1+=2- makes the 0→2 reachability transitively // satisfied, so the optimal dummy count is 0. - let dag = DirectedGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let dag = DirectedGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); let problem = MinimumDummyActivitiesPert::new(dag); let solution = BruteForce::new().solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&solution).unwrap(), Min(Some(0))); diff --git a/src/unit_tests/models/graph/minimum_edge_cost_flow.rs b/src/unit_tests/models/graph/minimum_edge_cost_flow.rs index 6253a1c80..f3163d3d8 100644 --- a/src/unit_tests/models/graph/minimum_edge_cost_flow.rs +++ b/src/unit_tests/models/graph/minimum_edge_cost_flow.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::DirectedGraph; use crate::traits::Problem; use crate::types::Min; @@ -11,26 +10,28 @@ use crate::types::Min; /// Optimal: route via v2 (1 unit) and v3 (2 units) → cost = 1 + 2 = 3 fn issue_instance() -> MinimumEdgeCostFlow { MinimumEdgeCostFlow::new( - DirectedGraph::new(5, vec![(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4)]), + DirectedGraph::new(5, vec![(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4)]).unwrap(), vec![3, 1, 2, 0, 0, 0], vec![2, 2, 2, 2, 2, 2], 0, 4, 3, ) + .unwrap() } /// Small 3-vertex instance: s=0, t=2, R=2. /// Arc (0,1) cap=1, (1,2) cap=1 — cannot route 2 units. fn infeasible_instance() -> MinimumEdgeCostFlow { MinimumEdgeCostFlow::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1, 1], vec![1, 1], 0, 2, 2, ) + .unwrap() } #[test] @@ -44,7 +45,10 @@ fn test_minimum_edge_cost_flow_creation() { assert_eq!(problem.max_capacity(), 2); assert_eq!(problem.prices(), &[3, 1, 2, 0, 0, 0]); assert_eq!(problem.capacities(), &[2, 2, 2, 2, 2, 2]); - assert_eq!(problem.dimensions(), vec![3, 3, 3, 3, 3, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3, 3, 3, 3, 3] + ); assert_eq!( ::NAME, "MinimumEdgeCostFlow" @@ -136,7 +140,15 @@ fn test_minimum_edge_cost_flow_serialization() { #[test] fn test_minimum_edge_cost_flow_max_capacity_empty() { - let problem = MinimumEdgeCostFlow::new(DirectedGraph::new(2, vec![]), vec![], vec![], 0, 1, 0); + let problem = MinimumEdgeCostFlow::new( + DirectedGraph::new(2, vec![]).unwrap(), + vec![], + vec![], + 0, + 1, + 0, + ) + .unwrap(); assert_eq!(problem.max_capacity(), 0); } 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 649abd44b..0779e90d4 100644 --- a/src/unit_tests/models/graph/minimum_feedback_arc_set.rs +++ b/src/unit_tests/models/graph/minimum_feedback_arc_set.rs @@ -1,10 +1,9 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_arc_weights() { let p = MinimumFeedbackArcSet::try_from(MinimumFeedbackArcSetCreateSpec { - graph: DirectedGraph::new(2, vec![(0, 1)]), + graph: DirectedGraph::new(2, vec![(0, 1)]).unwrap(), weights: None, }) .unwrap(); @@ -30,19 +29,28 @@ fn test_minimum_feedback_arc_set_creation() { (5, 3), (3, 0), ], - ); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 9]); + ) + .unwrap(); + let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 9]).unwrap(); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_arcs(), 9); - assert_eq!(problem.dimensions().len(), 9); - assert!(problem.dimensions().iter().all(|&d| d == 2)); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 9 + ); + assert!(crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .iter() + .all(|&d| d == 2)); } #[test] fn test_minimum_feedback_arc_set_evaluation_valid() { // Simple cycle: 0->1->2->0 - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]).unwrap(); // Remove arc 2->0 (index 2) -> breaks the cycle let config = vec![false, false, true]; @@ -66,8 +74,8 @@ fn test_minimum_feedback_arc_set_evaluation_valid() { #[test] fn test_minimum_feedback_arc_set_evaluation_invalid() { // Simple cycle: 0->1->2->0 - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]).unwrap(); // Remove no arcs -> cycle remains -> invalid let config = vec![false, false, false]; @@ -78,8 +86,8 @@ fn test_minimum_feedback_arc_set_evaluation_invalid() { #[test] fn test_minimum_feedback_arc_set_dag() { // Already a DAG: 0->1->2 - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 2]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 2]).unwrap(); // Remove no arcs -> already acyclic let config = vec![false, false]; @@ -91,8 +99,8 @@ fn test_minimum_feedback_arc_set_dag() { #[test] fn test_minimum_feedback_arc_set_solver_simple_cycle() { // Simple cycle: 0->1->2->0 - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]).unwrap(); let solutions = BruteForce::new().find_all_witnesses(&problem).unwrap(); // Minimum FAS has size 1 (remove any one arc) @@ -119,8 +127,9 @@ fn test_minimum_feedback_arc_set_solver_issue_example() { (5, 3), // a7 (3, 0), // a8 ], - ); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 9]); + ) + .unwrap(); + let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 9]).unwrap(); let solution = BruteForce::new().solve(&problem).unwrap().unwrap(); // The optimal FAS has size 2 @@ -136,8 +145,8 @@ fn test_minimum_feedback_arc_set_weighted() { // Cycle: 0->1->2->0 with weights [10, 1, 1] // Arc 0 (0->1) costs 10, arcs 1,2 cost 1 each // Optimal: remove arc 1 or arc 2 (cost 1), NOT arc 0 (cost 10) - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackArcSet::new(graph, vec![10i64, 1, 1]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackArcSet::new(graph, vec![10i64, 1, 1]).unwrap(); let solution = BruteForce::new().solve(&problem).unwrap().unwrap(); let result = problem.evaluate(&solution).unwrap(); @@ -150,8 +159,8 @@ fn test_minimum_feedback_arc_set_weighted() { #[test] fn test_minimum_feedback_arc_set_is_valid_solution() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]).unwrap(); // Valid: remove one arc from the cycle assert!(problem.is_valid_solution(&[false, false, true])); @@ -169,8 +178,8 @@ fn test_minimum_feedback_arc_set_problem_name() { #[test] fn test_minimum_feedback_arc_set_serialization() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]).unwrap(); let json = serde_json::to_string(&problem).unwrap(); let deserialized: MinimumFeedbackArcSet = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.num_vertices(), 3); @@ -180,8 +189,8 @@ fn test_minimum_feedback_arc_set_serialization() { #[test] fn test_minimum_feedback_arc_set_two_disjoint_cycles() { // Two disjoint cycles: 0->1->0 and 2->3->2 - let graph = DirectedGraph::new(4, vec![(0, 1), (1, 0), (2, 3), (3, 2)]); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 4]); + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 0), (2, 3), (3, 2)]).unwrap(); + let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 4]).unwrap(); let solution = BruteForce::new().solve(&problem).unwrap().unwrap(); // Need to remove at least one arc from each cycle -> size 2 @@ -190,20 +199,39 @@ fn test_minimum_feedback_arc_set_two_disjoint_cycles() { #[test] fn test_minimum_feedback_arc_set_parameter_getters() { - let graph = DirectedGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 5]); + let graph = DirectedGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(); + let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 5]).unwrap(); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_arcs(), 5); } #[test] fn test_minimum_feedback_arc_set_accessors() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let mut problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let mut problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]).unwrap(); assert!(problem.is_weighted()); // i64 type → true assert_eq!(problem.weights(), &[1, 1, 1]); - problem.set_weights(vec![2, 3, 4]); + problem.set_weights(vec![2, 3, 4]).unwrap(); assert_eq!(problem.weights(), &[2, 3, 4]); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = DirectedGraph::new(2, vec![(0, 1)]).unwrap(); + assert!(MinimumFeedbackArcSet::new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "weights": []}); + assert!(serde_json::from_value::>(json).is_err()); +} + +#[test] +fn rejected_weight_update_preserves_instance() { + let mut problem = + MinimumFeedbackArcSet::new(DirectedGraph::new(2, vec![(0, 1)]).unwrap(), vec![3i64; 1]) + .unwrap(); + let before = serde_json::to_value(&problem).unwrap(); + assert!(problem.set_weights(vec![]).is_err()); + assert_eq!(serde_json::to_value(&problem).unwrap(), before); + problem.set_weights(vec![4; 1]).unwrap(); +} 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 cc1e4005a..a616b36b4 100644 --- a/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs +++ b/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs @@ -1,10 +1,9 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_vertex_weights() { let p = MinimumFeedbackVertexSet::::try_from(MinimumFeedbackVertexSetCreateSpec { - graph: DirectedGraph::new(2, vec![(0, 1)]), + graph: DirectedGraph::new(2, vec![(0, 1)]).unwrap(), weights: None, }) .unwrap(); @@ -44,15 +43,19 @@ fn example_graph() -> DirectedGraph { (8, 2), ], ) + .unwrap() } #[test] fn test_minimum_feedback_vertex_set_basic() { let graph = example_graph(); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 9]); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 9]).unwrap(); // dims should be [2; 9] - assert_eq!(problem.dimensions(), vec![2usize; 9]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2usize; 9] + ); // Valid FVS: {0, 3, 8} → config = [1,0,0,1,0,0,0,0,1] let config_valid = vec![true, false, false, true, false, false, false, false, true]; @@ -72,7 +75,7 @@ fn test_minimum_feedback_vertex_set_basic() { #[test] fn test_minimum_feedback_vertex_set_serialization() { let graph = example_graph(); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 9]); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 9]).unwrap(); let json = serde_json::to_string(&problem).expect("serialization failed"); let deserialized: MinimumFeedbackVertexSet = @@ -86,7 +89,7 @@ fn test_minimum_feedback_vertex_set_serialization() { #[test] fn test_minimum_feedback_vertex_set_solver() { let graph = example_graph(); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 9]); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 9]).unwrap(); let solver = BruteForce::new(); let best = solver.solve(&problem).unwrap(); @@ -103,8 +106,8 @@ fn test_minimum_feedback_vertex_set_solver() { #[test] fn test_minimum_feedback_vertex_set_dag() { // A DAG: 0 → 1 → 2 - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]).unwrap(); // Empty set (all zeros) is a valid FVS — graph is already a DAG let config_empty = vec![false, false, false]; @@ -117,7 +120,7 @@ fn test_minimum_feedback_vertex_set_dag() { fn test_minimum_feedback_vertex_set_all_selected() { // Selecting all vertices always yields a valid (but suboptimal) FVS let graph = example_graph(); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 9]); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 9]).unwrap(); let config_all = vec![true; 9]; let result = problem.evaluate(&config_all).unwrap(); @@ -127,22 +130,22 @@ fn test_minimum_feedback_vertex_set_all_selected() { #[test] fn test_minimum_feedback_vertex_set_accessors() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let mut problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let mut problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]).unwrap(); assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_arcs(), 3); assert!(problem.is_weighted()); // set_weights - problem.set_weights(vec![2, 3, 4]); + problem.set_weights(vec![2, 3, 4]).unwrap(); assert_eq!(problem.weights(), &[2, 3, 4]); } #[test] fn test_minimum_feedback_vertex_set_is_valid_solution() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]).unwrap(); // Valid FVS: remove vertex 0 assert!(problem.is_valid_solution(&[1, 0, 0])); @@ -154,8 +157,8 @@ fn test_minimum_feedback_vertex_set_is_valid_solution() { #[test] fn test_minimum_feedback_vertex_set_evaluate_wrong_length() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]).unwrap(); // Wrong length config returns Invalid assert!(matches!( @@ -196,8 +199,9 @@ fn test_minimum_feedback_vertex_set_paper_example() { let graph = DirectedGraph::new( 5, vec![(0, 1), (1, 2), (2, 0), (0, 3), (3, 4), (4, 1), (4, 2)], - ); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 5]); + ) + .unwrap(); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 5]).unwrap(); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_arcs(), 7); @@ -222,7 +226,7 @@ fn test_minimum_feedback_vertex_set_paper_example() { fn test_minimum_feedback_vertex_set_unit_create_and_roundtrip() { use crate::types::One; let source = MinimumFeedbackVertexSet::::try_from(MinimumFeedbackVertexSetCreateSpec { - graph: DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]), + graph: DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(), weights: None, }) .unwrap(); @@ -236,9 +240,28 @@ fn test_minimum_feedback_vertex_set_unit_create_and_roundtrip() { ); assert!( MinimumFeedbackVertexSet::::try_from(MinimumFeedbackVertexSetCreateSpec { - graph: DirectedGraph::new(2, vec![]), + graph: DirectedGraph::new(2, vec![]).unwrap(), weights: Some(vec![One]), }) .is_err() ); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = DirectedGraph::new(2, vec![(0, 1)]).unwrap(); + assert!(MinimumFeedbackVertexSet::new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "weights": []}); + assert!(serde_json::from_value::>(json).is_err()); +} + +#[test] +fn rejected_weight_update_preserves_instance() { + let mut problem = + MinimumFeedbackVertexSet::new(DirectedGraph::new(2, vec![(0, 1)]).unwrap(), vec![3i64; 2]) + .unwrap(); + let before = serde_json::to_value(&problem).unwrap(); + assert!(problem.set_weights(vec![]).is_err()); + assert_eq!(serde_json::to_value(&problem).unwrap(), before); + problem.set_weights(vec![4; 2]).unwrap(); +} diff --git a/src/unit_tests/models/graph/minimum_geometric_connected_dominating_set.rs b/src/unit_tests/models/graph/minimum_geometric_connected_dominating_set.rs index a5a1c6535..6236476ae 100644 --- a/src/unit_tests/models/graph/minimum_geometric_connected_dominating_set.rs +++ b/src/unit_tests/models/graph/minimum_geometric_connected_dominating_set.rs @@ -10,8 +10,11 @@ fn test_creation_and_getters() { assert_eq!(problem.num_points(), 3); assert!((problem.radius() - 1.5).abs() < f64::EPSILON); assert_eq!(problem.points().len(), 3); - assert_eq!(problem.num_variables(), 3); - assert_eq!(problem.dimensions(), vec![2; 3]); + assert_eq!(problem.num_variables().unwrap(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 3] + ); } #[test] diff --git a/src/unit_tests/models/graph/minimum_graph_bandwidth.rs b/src/unit_tests/models/graph/minimum_graph_bandwidth.rs index bdfa58ec7..8c98573e0 100644 --- a/src/unit_tests/models/graph/minimum_graph_bandwidth.rs +++ b/src/unit_tests/models/graph/minimum_graph_bandwidth.rs @@ -1,19 +1,18 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Min; /// Star graph S4: center 0 connected to 1, 2, 3 fn star_example() -> MinimumGraphBandwidth { - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(); MinimumGraphBandwidth::new(graph) } /// Path graph P4: 0-1-2-3 fn path_example() -> MinimumGraphBandwidth { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); MinimumGraphBandwidth::new(graph) } @@ -22,7 +21,10 @@ fn test_minimumgraphbandwidth_creation() { let problem = star_example(); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); - assert_eq!(problem.dimensions(), vec![4, 4, 4, 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 4, 4, 4] + ); } #[test] @@ -104,9 +106,12 @@ fn test_minimumgraphbandwidth_serialization() { #[test] fn test_minimumgraphbandwidth_single_vertex() { - let graph = SimpleGraph::new(1, vec![]); + let graph = SimpleGraph::new(1, vec![]).unwrap(); let problem = MinimumGraphBandwidth::new(graph); - assert_eq!(problem.dimensions(), vec![1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1] + ); assert_eq!(problem.evaluate(&vec![0]).unwrap(), Min(Some(0))); assert_eq!(problem.bandwidth(&[0]).unwrap(), Some(0)); } @@ -114,7 +119,7 @@ fn test_minimumgraphbandwidth_single_vertex() { #[test] fn test_minimumgraphbandwidth_empty_graph() { // No edges: any permutation has bandwidth 0 - let graph = SimpleGraph::new(3, vec![]); + let graph = SimpleGraph::new(3, vec![]).unwrap(); let problem = MinimumGraphBandwidth::new(graph); let solver = BruteForce::new(); @@ -133,7 +138,7 @@ fn test_minimumgraphbandwidth_empty_graph() { fn test_minimumgraphbandwidth_complete_graph_k4() { // K4: bandwidth is always 3 (max position difference in any permutation) // Actually for K4, bandwidth = n-1 = 3 for any arrangement since edge (first, last) exists. - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); let problem = MinimumGraphBandwidth::new(graph); let solver = BruteForce::new(); diff --git a/src/unit_tests/models/graph/minimum_intersection_graph_basis.rs b/src/unit_tests/models/graph/minimum_intersection_graph_basis.rs index 17fd4e160..8a7971f7d 100644 --- a/src/unit_tests/models/graph/minimum_intersection_graph_basis.rs +++ b/src/unit_tests/models/graph/minimum_intersection_graph_basis.rs @@ -7,20 +7,23 @@ use crate::types::Min; #[test] fn test_minimum_intersection_graph_basis_creation() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MinimumIntersectionGraphBasis::new(graph); assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_edges(), 2); // 3 vertices * 2 edges = 6 binary variables - assert_eq!(problem.num_variables(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!(problem.num_variables().unwrap(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); } #[test] fn test_minimum_intersection_graph_basis_p3() { // Path P3: 0-1-2, edges (0,1) and (1,2) // Intersection number = 2: S[0]={0}, S[1]={0,1}, S[2]={1} - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MinimumIntersectionGraphBasis::new(graph); // Valid config: S[0]={0}, S[1]={0,1}, S[2]={1} -> [1,0, 1,1, 0,1] @@ -37,7 +40,7 @@ fn test_minimum_intersection_graph_basis_p3() { fn test_minimum_intersection_graph_basis_single_edge() { // Single edge: 0-1 // Intersection number = 1: S[0]={0}, S[1]={0} - let graph = SimpleGraph::new(2, vec![(0, 1)]); + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); let problem = MinimumIntersectionGraphBasis::new(graph); // Valid: S[0]={0}, S[1]={0} -> [1, 1] @@ -62,11 +65,14 @@ fn test_minimum_intersection_graph_basis_triangle() { // Triangle K3: edges (0,1),(1,2),(0,2) // Intersection number = 1 for K3: all vertices share one element. // S[0]={0}, S[1]={0}, S[2]={0} — all pairs intersect, which matches K3. - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); let problem = MinimumIntersectionGraphBasis::new(graph); // 3 vertices * 3 edges = 9 binary variables - assert_eq!(problem.dimensions(), vec![2; 9]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 9] + ); // Valid: S[0]={0}, S[1]={0}, S[2]={0} // config: v0: [1,0,0], v1: [1,0,0], v2: [1,0,0] @@ -85,7 +91,7 @@ fn test_minimum_intersection_graph_basis_triangle() { #[test] fn test_minimum_intersection_graph_basis_empty_graph() { // No edges: universe size 0 - let graph = SimpleGraph::new(3, vec![]); + let graph = SimpleGraph::new(3, vec![]).unwrap(); let problem = MinimumIntersectionGraphBasis::new(graph); assert_eq!( problem.evaluate(&vec![vec![], vec![], vec![]]).unwrap(), @@ -95,7 +101,7 @@ fn test_minimum_intersection_graph_basis_empty_graph() { #[test] fn test_minimum_intersection_graph_basis_wrong_length() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MinimumIntersectionGraphBasis::new(graph); assert!(problem .evaluate(&vec![vec![true, false], vec![true]]) @@ -106,7 +112,7 @@ fn test_minimum_intersection_graph_basis_wrong_length() { fn test_minimum_intersection_graph_basis_invalid_nonadjacent_intersect() { // P3: edges (0,1),(1,2). Vertices 0 and 2 are NOT adjacent. // If S[0] and S[2] intersect, it's invalid. - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MinimumIntersectionGraphBasis::new(graph); // S[0]={0,1}, S[1]={0,1}, S[2]={0,1} -> 0 and 2 share elements -> invalid @@ -118,7 +124,7 @@ fn test_minimum_intersection_graph_basis_invalid_nonadjacent_intersect() { fn test_minimum_intersection_graph_basis_invalid_edge_not_covered() { // P3: edges (0,1),(1,2). // S[0]={0}, S[1]={1}, S[2]={1} -> edge (0,1) not covered (no intersection) - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MinimumIntersectionGraphBasis::new(graph); let config = vec![vec![true, false], vec![false, true], vec![false, true]]; diff --git a/src/unit_tests/models/graph/minimum_maximal_matching.rs b/src/unit_tests/models/graph/minimum_maximal_matching.rs index 4a15078d7..dc0e62781 100644 --- a/src/unit_tests/models/graph/minimum_maximal_matching.rs +++ b/src/unit_tests/models/graph/minimum_maximal_matching.rs @@ -7,11 +7,11 @@ use crate::types::Min; #[test] fn test_minimum_maximal_matching_creation() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = MinimumMaximalMatching::new(graph); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); - assert_eq!(problem.num_variables(), 3); + assert_eq!(problem.num_variables().unwrap(), 3); } #[test] @@ -20,7 +20,7 @@ fn test_minimum_maximal_matching_evaluate_valid() { // config [0,1,0]: select edge (1,2). Is it maximal? // Edge (0,1): shares vertex 1 with (1,2) ✓ blocked // Edge (2,3): shares vertex 2 with (1,2) ✓ blocked - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = MinimumMaximalMatching::new(graph); assert_eq!( problem.evaluate(&vec![false, true, false]).unwrap(), @@ -32,7 +32,7 @@ fn test_minimum_maximal_matching_evaluate_valid() { fn test_minimum_maximal_matching_evaluate_not_maximal() { // Path P4: edges (0,1),(1,2),(2,3) // config [1,0,0]: select only (0,1). Edge (2,3) is not blocked — not maximal. - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = MinimumMaximalMatching::new(graph); assert_eq!( problem.evaluate(&vec![true, false, false]).unwrap(), @@ -44,7 +44,7 @@ fn test_minimum_maximal_matching_evaluate_not_maximal() { fn test_minimum_maximal_matching_evaluate_not_matching() { // Triangle: edges (0,1),(1,2),(0,2) // config [1,1,0]: select (0,1) and (1,2) — vertex 1 shared → not a matching. - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); let problem = MinimumMaximalMatching::new(graph); assert_eq!( problem.evaluate(&vec![true, true, false]).unwrap(), @@ -54,7 +54,7 @@ fn test_minimum_maximal_matching_evaluate_not_matching() { #[test] fn test_minimum_maximal_matching_evaluate_wrong_length() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MinimumMaximalMatching::new(graph); // Provide config of wrong length assert!(matches!( @@ -66,7 +66,7 @@ fn test_minimum_maximal_matching_evaluate_wrong_length() { #[test] fn test_minimum_maximal_matching_empty_graph() { // No edges: empty config is a valid (vacuously maximal) matching of size 0. - let graph = SimpleGraph::new(3, vec![]); + let graph = SimpleGraph::new(3, vec![]).unwrap(); let problem = MinimumMaximalMatching::new(graph); assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } @@ -75,7 +75,7 @@ fn test_minimum_maximal_matching_empty_graph() { fn test_minimum_maximal_matching_path_p6_solver() { // Path P6: 6 vertices, 5 edges. // Optimal minimum maximal matching has size 2, e.g. {(1,2),(3,4)}. - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]); + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]).unwrap(); let problem = MinimumMaximalMatching::new(graph); let solver = BruteForce::new(); let best = solver.solve(&problem).unwrap().unwrap(); @@ -85,7 +85,7 @@ fn test_minimum_maximal_matching_path_p6_solver() { #[test] fn test_minimum_maximal_matching_canonical_example() { // Canonical example: P6 with config [0,1,0,1,0] → edges (1,2) and (3,4). - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]); + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]).unwrap(); let problem = MinimumMaximalMatching::new(graph); let config = vec![false, true, false, true, false]; assert_eq!(problem.evaluate(&config).unwrap(), Min(Some(2))); @@ -94,7 +94,7 @@ fn test_minimum_maximal_matching_canonical_example() { #[test] fn test_minimum_maximal_matching_triangle() { // Triangle: any single edge is a maximal matching (both remaining edges are blocked). - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); let problem = MinimumMaximalMatching::new(graph); let solver = BruteForce::new(); let best = solver.solve(&problem).unwrap().unwrap(); @@ -105,7 +105,7 @@ fn test_minimum_maximal_matching_triangle() { fn test_minimum_maximal_matching_star() { // Star K_{1,3}: center 0 connected to 1,2,3. // Any single edge from center is a maximal matching. - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(); let problem = MinimumMaximalMatching::new(graph); let solver = BruteForce::new(); let best = solver.solve(&problem).unwrap().unwrap(); @@ -114,7 +114,7 @@ fn test_minimum_maximal_matching_star() { #[test] fn test_minimum_maximal_matching_serialization() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = MinimumMaximalMatching::new(graph); let json = serde_json::to_string(&problem).unwrap(); let deserialized: MinimumMaximalMatching = serde_json::from_str(&json).unwrap(); diff --git a/src/unit_tests/models/graph/minimum_metric_dimension.rs b/src/unit_tests/models/graph/minimum_metric_dimension.rs index 6e4764d1f..7dcf6f1e6 100644 --- a/src/unit_tests/models/graph/minimum_metric_dimension.rs +++ b/src/unit_tests/models/graph/minimum_metric_dimension.rs @@ -7,18 +7,21 @@ use crate::types::Min; #[test] fn test_minimum_metric_dimension_creation() { - let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(); let problem = MinimumMetricDimension::new(graph); assert_eq!(problem.graph().num_vertices(), 5); assert_eq!(problem.graph().num_edges(), 6); - assert_eq!(problem.num_variables(), 5); - assert_eq!(problem.dimensions(), vec![2; 5]); + assert_eq!(problem.num_variables().unwrap(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); } #[test] fn test_minimum_metric_dimension_evaluate_optimal() { // House graph: selecting vertices 0 and 1 forms a resolving set of size 2 - let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(); let problem = MinimumMetricDimension::new(graph); let config = vec![true, true, false, false, false]; // select v0, v1 let result = problem.evaluate(&config).unwrap(); @@ -29,7 +32,7 @@ fn test_minimum_metric_dimension_evaluate_optimal() { #[test] fn test_minimum_metric_dimension_evaluate_non_resolving() { // House graph: selecting only v2 should not resolve all pairs - let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(); let problem = MinimumMetricDimension::new(graph); // v2 alone: d(0,2)=1, d(1,2)=2, d(3,2)=1, d(4,2)=1 // vertices 0 and 3 both have distance 1 to v2 -> not resolving @@ -40,7 +43,7 @@ fn test_minimum_metric_dimension_evaluate_non_resolving() { #[test] fn test_minimum_metric_dimension_evaluate_empty_selection() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MinimumMetricDimension::new(graph); let config = vec![false, false, false]; let result = problem.evaluate(&config).unwrap(); @@ -50,7 +53,7 @@ fn test_minimum_metric_dimension_evaluate_empty_selection() { #[test] fn test_minimum_metric_dimension_evaluate_all_selected() { // Selecting all vertices is always resolving (trivially) - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MinimumMetricDimension::new(graph); let config = vec![true, true, true]; let result = problem.evaluate(&config).unwrap(); @@ -61,7 +64,7 @@ fn test_minimum_metric_dimension_evaluate_all_selected() { #[test] fn test_minimum_metric_dimension_solver() { // House graph: minimum resolving set has size 2 - let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(); let problem = MinimumMetricDimension::new(graph); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap().unwrap(); @@ -75,7 +78,7 @@ fn test_minimum_metric_dimension_path_graph() { // Path graph P3: 0-1-2 // Metric dimension of a path is 1 (either endpoint resolves) // d(0,0)=0, d(1,0)=1, d(2,0)=2 -> all distinct -> {0} resolves - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MinimumMetricDimension::new(graph); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap().unwrap(); @@ -87,7 +90,7 @@ fn test_minimum_metric_dimension_path_graph() { fn test_minimum_metric_dimension_complete_graph() { // K4: metric dimension of K_n is n-1 (all distances are 1, so any pair // at distance 1 from each other needs a resolving vertex that is one of them) - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); let problem = MinimumMetricDimension::new(graph); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap().unwrap(); @@ -97,7 +100,7 @@ fn test_minimum_metric_dimension_complete_graph() { #[test] fn test_minimum_metric_dimension_serialization() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = MinimumMetricDimension::new(graph); let json = serde_json::to_string(&problem).unwrap(); let deserialized: MinimumMetricDimension = serde_json::from_str(&json).unwrap(); @@ -114,7 +117,7 @@ fn test_minimum_metric_dimension_serialization() { #[test] fn test_minimum_metric_dimension_parameter_getters() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = MinimumMetricDimension::new(graph); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); @@ -123,7 +126,7 @@ fn test_minimum_metric_dimension_parameter_getters() { #[test] fn test_minimum_metric_dimension_cycle() { // C5: metric dimension of a cycle C_n with n >= 3 is 2 - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(); let problem = MinimumMetricDimension::new(graph); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap().unwrap(); diff --git a/src/unit_tests/models/graph/minimum_multiway_cut.rs b/src/unit_tests/models/graph/minimum_multiway_cut.rs index c9831cc6e..fc59c5731 100644 --- a/src/unit_tests/models/graph/minimum_multiway_cut.rs +++ b/src/unit_tests/models/graph/minimum_multiway_cut.rs @@ -1,11 +1,10 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_invalid_terminals() { assert_eq!(MinimumMultiwayCutCreateSpec::FIELDS[1].name, "terminals"); let result = MinimumMultiwayCut::try_from(MinimumMultiwayCutCreateSpec { - graph: SimpleGraph::new(2, vec![(0, 1)]), + graph: SimpleGraph::new(2, vec![(0, 1)]).unwrap(), terminals: vec![0, 0], edge_weights: vec![1], }); @@ -18,9 +17,14 @@ use crate::types::Min; #[test] fn test_minimummultiwaycut_creation() { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); - assert_eq!(problem.dimensions().len(), 6); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]).unwrap(); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 6 + ); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 6); assert_eq!(problem.num_terminals(), 3); @@ -30,8 +34,8 @@ fn test_minimummultiwaycut_creation() { fn test_minimummultiwaycut_evaluate_valid() { // Issue example: 5 vertices, terminals {0,2,4} // Edges: (0,1)w=2, (1,2)w=3, (2,3)w=1, (3,4)w=2, (0,4)w=4, (1,3)w=5 - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]).unwrap(); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]).unwrap(); // Optimal cut: remove edges (0,1), (3,4), (0,4) => indices 0, 3, 4 // config: [1, 0, 0, 1, 1, 0] => weight 2 + 2 + 4 = 8 @@ -42,8 +46,8 @@ fn test_minimummultiwaycut_evaluate_valid() { #[test] fn test_minimummultiwaycut_evaluate_invalid() { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]).unwrap(); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]).unwrap(); // No edges cut: all terminals connected => invalid let config = vec![false, false, false, false, false, false]; @@ -54,8 +58,8 @@ fn test_minimummultiwaycut_evaluate_invalid() { #[test] fn test_minimummultiwaycut_brute_force() { // Issue example: optimal cut has weight 8 - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]).unwrap(); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -78,8 +82,8 @@ fn test_minimummultiwaycut_two_terminals() { // k=2: classical min s-t cut. Path graph: 0-1-2, terminals {0,2} // Edges: (0,1)w=3, (1,2)w=5 // Min cut: remove (0,1) with weight 3 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![3i64, 5]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![3i64, 5]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -90,8 +94,8 @@ fn test_minimummultiwaycut_two_terminals() { #[test] fn test_minimummultiwaycut_all_edges_cut() { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]).unwrap(); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]).unwrap(); let config = vec![true, true, true, true, true, true]; let result = problem.evaluate(&config).unwrap(); assert_eq!(result, Min(Some(2 + 3 + 1 + 2 + 4 + 5))); @@ -101,8 +105,8 @@ fn test_minimummultiwaycut_all_edges_cut() { fn test_minimummultiwaycut_already_disconnected() { // Terminals already in different components => empty cut is valid // Graph: 0-1 2-3, terminals {0, 2} - let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i64, 1]); + let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i64, 1]).unwrap(); let config = vec![false, false]; let result = problem.evaluate(&config).unwrap(); assert_eq!(result, Min(Some(0))); @@ -116,8 +120,8 @@ fn test_minimummultiwaycut_already_disconnected() { #[test] fn test_minimummultiwaycut_serialization() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i64, 2]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i64, 2]).unwrap(); let json = serde_json::to_string(&problem).unwrap(); let restored: MinimumMultiwayCut = serde_json::from_str(&json).unwrap(); assert_eq!(restored.num_vertices(), 3); @@ -134,45 +138,41 @@ fn test_minimummultiwaycut_name() { } #[test] -#[should_panic(expected = "edge_weights length must match num_edges")] -fn test_minimummultiwaycut_panic_wrong_weights_len() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i64]); +fn test_minimummultiwaycut_rejects_wrong_weights_len() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + assert!(MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i64]).is_err()); } #[test] -#[should_panic(expected = "need at least 2 terminals")] -fn test_minimummultiwaycut_panic_too_few_terminals() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - MinimumMultiwayCut::new(graph, vec![0], vec![1i64, 1]); +fn test_minimummultiwaycut_rejects_too_few_terminals() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + assert!(MinimumMultiwayCut::new(graph, vec![0], vec![1i64, 1]).is_err()); } #[test] -#[should_panic(expected = "duplicate terminal indices")] -fn test_minimummultiwaycut_panic_duplicate_terminals() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - MinimumMultiwayCut::new(graph, vec![0, 0], vec![1i64, 1]); +fn test_minimummultiwaycut_rejects_duplicate_terminals() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + assert!(MinimumMultiwayCut::new(graph, vec![0, 0], vec![1i64, 1]).is_err()); } #[test] -#[should_panic(expected = "terminal index out of bounds")] -fn test_minimummultiwaycut_panic_terminal_out_of_bounds() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - MinimumMultiwayCut::new(graph, vec![0, 10], vec![1i64, 1]); +fn test_minimummultiwaycut_rejects_terminal_out_of_bounds() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + assert!(MinimumMultiwayCut::new(graph, vec![0, 10], vec![1i64, 1]).is_err()); } #[test] fn test_minimummultiwaycut_getters() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![3i64, 5]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![3i64, 5]).unwrap(); assert_eq!(problem.graph().num_vertices(), 3); assert_eq!(problem.edge_weights(), &[3, 5]); } #[test] fn test_minimummultiwaycut_rejects_wrong_config_lengths() { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]).unwrap(); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]).unwrap(); let short_config = vec![true, false]; assert!(matches!( diff --git a/src/unit_tests/models/graph/minimum_sum_multicenter.rs b/src/unit_tests/models/graph/minimum_sum_multicenter.rs index e636dee31..74441815a 100644 --- a/src/unit_tests/models/graph/minimum_sum_multicenter.rs +++ b/src/unit_tests/models/graph/minimum_sum_multicenter.rs @@ -1,13 +1,12 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; #[test] fn test_min_sum_multicenter_creation() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); - let problem = MinimumSumMulticenter::new(graph, vec![1i64; 4], vec![1i64; 3], 2); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); + let problem = MinimumSumMulticenter::new(graph, vec![1i64; 4], vec![1i64; 3], 2).unwrap(); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); assert_eq!(problem.k(), 2); @@ -17,8 +16,8 @@ fn test_min_sum_multicenter_creation() { #[test] fn test_min_sum_multicenter_parameter_getters() { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); - let problem = MinimumSumMulticenter::new(graph, vec![1i64; 5], vec![1i64; 4], 2); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(); + let problem = MinimumSumMulticenter::new(graph, vec![1i64; 5], vec![1i64; 4], 2).unwrap(); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 4); assert_eq!(problem.num_centers(), 2); @@ -27,8 +26,8 @@ fn test_min_sum_multicenter_parameter_getters() { #[test] fn test_min_sum_multicenter_evaluate_path() { // Path: 0-1-2, unit weights and lengths, K=1 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 1); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 1).unwrap(); // Center at vertex 1: distances = [1, 0, 1], total = 2 let result = problem.evaluate(&vec![false, true, false]).unwrap(); @@ -43,8 +42,8 @@ fn test_min_sum_multicenter_evaluate_path() { #[test] fn test_min_sum_multicenter_wrong_k() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 2); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 2).unwrap(); // Only 1 center selected when K=2 let result = problem.evaluate(&vec![false, true, false]).unwrap(); @@ -62,8 +61,8 @@ fn test_min_sum_multicenter_wrong_k() { #[test] fn test_min_sum_multicenter_weighted() { // Path: 0-1-2, vertex weights = [3, 1, 2], edge lengths = [1, 1], K=1 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumSumMulticenter::new(graph, vec![3i64, 1, 2], vec![1i64; 2], 1); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumSumMulticenter::new(graph, vec![3i64, 1, 2], vec![1i64; 2], 1).unwrap(); // Center at 0: distances = [0, 1, 2], total = 3*0 + 1*1 + 2*2 = 5 assert_eq!( @@ -96,8 +95,8 @@ fn test_min_sum_multicenter_weighted() { #[test] fn test_min_sum_multicenter_weighted_edges() { // Triangle: 0-1 (len 1), 1-2 (len 3), 0-2 (len 2), K=1 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let problem = MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1, 3, 2], 1); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let problem = MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1, 3, 2], 1).unwrap(); // Center at 0: d(0)=0, d(1)=1, d(2)=2, total=3 assert_eq!( @@ -121,8 +120,8 @@ fn test_min_sum_multicenter_weighted_edges() { #[test] fn test_min_sum_multicenter_two_centers() { // Path: 0-1-2-3-4, unit weights and lengths, K=2 - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); - let problem = MinimumSumMulticenter::new(graph, vec![1i64; 5], vec![1i64; 4], 2); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(); + let problem = MinimumSumMulticenter::new(graph, vec![1i64; 5], vec![1i64; 4], 2).unwrap(); // Centers at {1, 3}: d = [1, 0, 1, 0, 1], total = 3 assert_eq!( @@ -158,8 +157,9 @@ fn test_min_sum_multicenter_solver() { (0, 6), (2, 5), ], - ); - let problem = MinimumSumMulticenter::new(graph, vec![1i64; 7], vec![1i64; 8], 2); + ) + .unwrap(); + let problem = MinimumSumMulticenter::new(graph, vec![1i64; 7], vec![1i64; 8], 2).unwrap(); let solver = BruteForce::new(); let best = solver.solve(&problem).unwrap().unwrap(); @@ -172,16 +172,16 @@ fn test_min_sum_multicenter_solver() { #[test] fn test_min_sum_multicenter_disconnected() { // Two disconnected components: 0-1 and 2-3, K=1 - let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]); - let problem = MinimumSumMulticenter::new(graph, vec![1i64; 4], vec![1i64; 2], 1); + let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(); + let problem = MinimumSumMulticenter::new(graph, vec![1i64; 4], vec![1i64; 2], 1).unwrap(); // Center at 0: vertex 2 and 3 are unreachable let result = problem.evaluate(&vec![true, false, false, false]).unwrap(); assert!(!result.is_valid()); // With K=2, centers at {0, 2}: all reachable - let graph2 = SimpleGraph::new(4, vec![(0, 1), (2, 3)]); - let problem2 = MinimumSumMulticenter::new(graph2, vec![1i64; 4], vec![1i64; 2], 2); + let graph2 = SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(); + let problem2 = MinimumSumMulticenter::new(graph2, vec![1i64; 4], vec![1i64; 2], 2).unwrap(); let result2 = problem2.evaluate(&vec![true, false, true, false]).unwrap(); assert!(result2.is_valid()); assert_eq!(result2.unwrap(), 2); // d = [0, 1, 0, 1] @@ -189,8 +189,8 @@ fn test_min_sum_multicenter_disconnected() { #[test] fn test_min_sum_multicenter_single_vertex() { - let graph = SimpleGraph::new(1, vec![]); - let problem = MinimumSumMulticenter::new(graph, vec![5i64], vec![], 1); + let graph = SimpleGraph::new(1, vec![]).unwrap(); + let problem = MinimumSumMulticenter::new(graph, vec![5i64], vec![], 1).unwrap(); let result = problem.evaluate(&vec![true]).unwrap(); assert!(result.is_valid()); assert_eq!(result.unwrap(), 0); // Only vertex is the center, distance = 0 @@ -199,39 +199,35 @@ fn test_min_sum_multicenter_single_vertex() { #[test] fn test_min_sum_multicenter_all_centers() { // K = num_vertices: all vertices are centers, total distance = 0 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 3); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 3).unwrap(); let result = problem.evaluate(&vec![true, true, true]).unwrap(); assert!(result.is_valid()); assert_eq!(result.unwrap(), 0); } #[test] -#[should_panic(expected = "vertex_weights length must match num_vertices")] fn test_min_sum_multicenter_wrong_vertex_weights_len() { - let graph = SimpleGraph::new(3, vec![(0, 1)]); - MinimumSumMulticenter::new(graph, vec![1i64; 2], vec![1i64; 1], 1); + let graph = SimpleGraph::new(3, vec![(0, 1)]).unwrap(); + assert!(MinimumSumMulticenter::new(graph, vec![1i64; 2], vec![1i64; 1], 1).is_err()); } #[test] -#[should_panic(expected = "edge_lengths length must match num_edges")] fn test_min_sum_multicenter_wrong_edge_lengths_len() { - let graph = SimpleGraph::new(3, vec![(0, 1)]); - MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 1); + let graph = SimpleGraph::new(3, vec![(0, 1)]).unwrap(); + assert!(MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 1).is_err()); } #[test] -#[should_panic(expected = "k must be positive")] fn test_min_sum_multicenter_k_zero() { - let graph = SimpleGraph::new(3, vec![(0, 1)]); - MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 1], 0); + let graph = SimpleGraph::new(3, vec![(0, 1)]).unwrap(); + assert!(MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 1], 0).is_err()); } #[test] -#[should_panic(expected = "k must not exceed num_vertices")] fn test_min_sum_multicenter_k_too_large() { - let graph = SimpleGraph::new(3, vec![(0, 1)]); - MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 1], 4); + let graph = SimpleGraph::new(3, vec![(0, 1)]).unwrap(); + assert!(MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 1], 4).is_err()); } #[test] @@ -249,8 +245,9 @@ fn test_min_sum_multicenter_paper_example() { (0, 6), (2, 5), ], - ); - let problem = MinimumSumMulticenter::new(graph, vec![1i64; 7], vec![1i64; 8], 2); + ) + .unwrap(); + let problem = MinimumSumMulticenter::new(graph, vec![1i64; 7], vec![1i64; 8], 2).unwrap(); // Optimal: centers at {2, 5}, config [0,0,1,0,0,1,0] // Distances: d(0)=2, d(1)=1, d(2)=0, d(3)=1, d(4)=1, d(5)=0, d(6)=1 @@ -269,16 +266,19 @@ fn test_min_sum_multicenter_paper_example() { #[test] fn test_min_sum_multicenter_dims() { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); - let problem = MinimumSumMulticenter::new(graph, vec![1i64; 5], vec![1i64; 4], 2); - assert_eq!(problem.dimensions(), vec![2; 5]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(); + let problem = MinimumSumMulticenter::new(graph, vec![1i64; 5], vec![1i64; 4], 2).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); } #[test] 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![1i64; 3], vec![1i64; 2], 1); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 1).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -288,8 +288,8 @@ fn test_min_sum_multicenter_find_all_witnesses() { #[test] fn test_min_sum_multicenter_serialization() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 1); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumSumMulticenter::new(graph, vec![1i64; 3], vec![1i64; 2], 1).unwrap(); let json = serde_json::to_string(&problem).unwrap(); let deserialized: MinimumSumMulticenter = diff --git a/src/unit_tests/models/graph/minimum_vertex_cover.rs b/src/unit_tests/models/graph/minimum_vertex_cover.rs index 809475eef..152fcfbb4 100644 --- a/src/unit_tests/models/graph/minimum_vertex_cover.rs +++ b/src/unit_tests/models/graph/minimum_vertex_cover.rs @@ -8,49 +8,51 @@ fn create_spec_rejects_weight_count_mismatch() { "weights" ); let result = MinimumVertexCover::try_from(MinimumVertexCoverCreateSpec { - graph: SimpleGraph::new(2, vec![(0, 1)]), + graph: SimpleGraph::new(2, vec![(0, 1)]).unwrap(), weights: Some(vec![1]), }); assert!(result.is_err()); } +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; -include!("../../jl_helpers.rs"); #[test] fn test_vertex_cover_creation() { let problem = MinimumVertexCover::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.num_variables(), 4); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] fn test_vertex_cover_with_weights() { - let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1, 2, 3]); + let problem = + MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1, 2, 3]).unwrap(); assert_eq!(problem.weights().to_vec(), vec![1, 2, 3]); } #[test] fn test_is_vertex_cover_function() { assert!(is_vertex_cover( - &SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), &[false, true, false] )); assert!(is_vertex_cover( - &SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), &[true, false, true] )); assert!(!is_vertex_cover( - &SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), &[true, false, false] )); assert!(!is_vertex_cover( - &SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), &[false, false, false] )); } @@ -61,8 +63,11 @@ fn test_complement_relationship() { use crate::models::graph::MaximumIndependentSet; let edges = vec![(0, 1), (1, 2), (2, 3)]; - let is_problem = MaximumIndependentSet::new(SimpleGraph::new(4, edges.clone()), vec![1i64; 4]); - let vc_problem = MinimumVertexCover::new(SimpleGraph::new(4, edges), vec![1i64; 4]); + let is_problem = + MaximumIndependentSet::new(SimpleGraph::new(4, edges.clone()).unwrap(), vec![1i64; 4]) + .unwrap(); + let vc_problem = + MinimumVertexCover::new(SimpleGraph::new(4, edges).unwrap(), vec![1i64; 4]).unwrap(); let solver = BruteForce::new(); @@ -81,20 +86,21 @@ fn test_complement_relationship() { #[should_panic(expected = "selected length must match num_vertices")] fn test_is_vertex_cover_wrong_len() { // Wrong length should panic - is_vertex_cover(&SimpleGraph::new(3, vec![(0, 1)]), &[true, false]); + is_vertex_cover(&SimpleGraph::new(3, vec![(0, 1)]).unwrap(), &[true, false]); } #[test] fn test_from_graph() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumVertexCover::new(graph, vec![1i64, 1, 1]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumVertexCover::new(graph, vec![1i64, 1, 1]).unwrap(); assert_eq!(problem.graph().num_vertices(), 3); assert_eq!(problem.graph().num_edges(), 2); } #[test] fn test_evaluate_rejects_invalid_configurations() { - let problem = MinimumVertexCover::new(SimpleGraph::new(2, vec![]), vec![1_i64, 1]); + let problem = + MinimumVertexCover::new(SimpleGraph::new(2, vec![]).unwrap(), vec![1_i64, 1]).unwrap(); assert!(matches!( problem.evaluate(&vec![true]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -111,14 +117,18 @@ fn test_evaluate_rejects_invalid_configurations() { #[test] fn test_from_graph_with_weights() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumVertexCover::new(graph, vec![1, 2, 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumVertexCover::new(graph, vec![1, 2, 3]).unwrap(); assert_eq!(problem.weights().to_vec(), vec![1, 2, 3]); } #[test] fn test_graph_accessor() { - let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); let graph = problem.graph(); assert_eq!(graph.num_vertices(), 3); assert_eq!(graph.num_edges(), 2); @@ -126,7 +136,11 @@ fn test_graph_accessor() { #[test] fn test_has_edge() { - let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); assert!(problem.graph().has_edge(0, 1)); assert!(problem.graph().has_edge(1, 0)); // Undirected assert!(problem.graph().has_edge(1, 2)); @@ -143,7 +157,8 @@ fn test_jl_parity_evaluation() { let nv = instance["instance"]["num_vertices"].as_u64().unwrap() as usize; let edges = jl_parse_edges(&instance["instance"]); let weights = jl_parse_i64_vec(&instance["instance"]["weights"]); - let problem = MinimumVertexCover::new(SimpleGraph::new(nv, edges), weights); + let problem = + MinimumVertexCover::new(SimpleGraph::new(nv, edges).unwrap(), weights).unwrap(); for eval in instance["evaluations"].as_array().unwrap() { let config = jl_parse_bool_config(&eval["config"]); let result = problem.evaluate(&config).unwrap(); @@ -174,7 +189,11 @@ fn test_jl_parity_evaluation() { #[test] fn test_is_valid_solution() { // Path graph: 0-1-2 - let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); // Valid: {1} covers both edges assert!(problem.is_valid_solution(&[false, true, false])); // Invalid: {0} doesn't cover edge (1,2) @@ -183,7 +202,11 @@ fn test_is_valid_solution() { #[test] fn test_parameter_getters() { - let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_edges(), 2); } @@ -191,8 +214,8 @@ fn test_parameter_getters() { #[test] fn test_mvc_paper_example() { // Paper: house graph, VC = {v_0, v_3, v_4}, weight = 3 - let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); - let problem = MinimumVertexCover::new(graph, vec![1i64; 5]); + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(); + let problem = MinimumVertexCover::new(graph, vec![1i64; 5]).unwrap(); let config = vec![true, false, false, true, true]; // {v_0, v_3, v_4} let result = problem.evaluate(&config).unwrap(); assert!(result.is_valid()); @@ -202,3 +225,16 @@ fn test_mvc_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap().unwrap(), 3); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::new(1, vec![]).unwrap(); + assert!(MinimumVertexCover::new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "weights": []}); + assert!(serde_json::from_value::>(json.clone()).is_err()); + let variant = std::collections::BTreeMap::from([ + ("graph".into(), "SimpleGraph".into()), + ("weight".into(), "i64".into()), + ]); + assert!(crate::registry::load_dyn("MinimumVertexCover", &variant, json).is_err()); +} diff --git a/src/unit_tests/models/graph/mixed_chinese_postman.rs b/src/unit_tests/models/graph/mixed_chinese_postman.rs index 5c8b3cbc8..4a141566a 100644 --- a/src/unit_tests/models/graph/mixed_chinese_postman.rs +++ b/src/unit_tests/models/graph/mixed_chinese_postman.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_infers_graph_and_default_weights() { @@ -26,7 +25,8 @@ fn sample_instance() -> MixedChinesePostman { 5, vec![(0, 1), (1, 2), (2, 3), (3, 0)], vec![(0, 2), (1, 3), (0, 4), (4, 2)], - ), + ) + .unwrap(), vec![2, 3, 1, 4], vec![2, 3, 1, 2], ) @@ -38,7 +38,8 @@ fn disconnected_instance() -> MixedChinesePostman { 6, vec![(0, 1), (1, 0), (2, 3)], vec![(0, 2), (1, 3), (3, 4), (4, 5), (5, 2)], - ), + ) + .unwrap(), vec![1, 1, 1], vec![1, 1, 5, 5, 5], ) @@ -51,7 +52,10 @@ fn test_mixed_chinese_postman_creation_and_accessors() { assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_arcs(), 4); assert_eq!(problem.num_edges(), 4); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); assert_eq!(problem.arc_weights(), &[2, 3, 1, 4]); assert_eq!(problem.edge_weights(), &[2, 3, 1, 2]); } @@ -83,8 +87,11 @@ fn test_mixed_chinese_postman_evaluate_connected_instance() { fn test_mixed_chinese_postman_single_edge_walk() { // V={0,1}, A=∅, E={{0,1}}, weight=1. // Walk 0→1→0: base cost 1, needs to balance so total cost is 2. - let problem = - MixedChinesePostman::new(MixedGraph::new(2, vec![], vec![(0, 1)]), vec![], vec![1]); + let problem = MixedChinesePostman::new( + MixedGraph::new(2, vec![], vec![(0, 1)]).unwrap(), + vec![], + vec![1], + ); assert_eq!(problem.evaluate(&vec![false]).unwrap(), Min(Some(2))); assert_eq!(problem.evaluate(&vec![true]).unwrap(), Min(Some(2))); @@ -97,7 +104,7 @@ fn test_mixed_chinese_postman_single_edge_walk() { fn test_mixed_chinese_postman_rejects_disconnected_graph() { // Two disconnected components {0,1} and {2,3}: no closed walk can cover all edges. let problem = MixedChinesePostman::new( - MixedGraph::new(4, vec![], vec![(0, 1), (2, 3)]), + MixedGraph::new(4, vec![], vec![(0, 1), (2, 3)]).unwrap(), vec![], vec![1, 1], ); @@ -169,7 +176,8 @@ fn test_mixed_chinese_postman_ignores_isolated_vertices() { 8, vec![(5, 3), (1, 4), (0, 1), (2, 4), (0, 5)], vec![(4, 2), (0, 4), (0, 2), (1, 3)], - ), + ) + .unwrap(), vec![4, 5, 1, 12, 9], vec![6, 1, 13, 7], ); diff --git a/src/unit_tests/models/graph/monochromatic_triangle.rs b/src/unit_tests/models/graph/monochromatic_triangle.rs index 54f7a56f8..3a7ce0d12 100644 --- a/src/unit_tests/models/graph/monochromatic_triangle.rs +++ b/src/unit_tests/models/graph/monochromatic_triangle.rs @@ -1,15 +1,13 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; fn k4_instance() -> MonochromaticTriangle { // K4: complete graph on 4 vertices, 6 edges - MonochromaticTriangle::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )) + MonochromaticTriangle::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), + ) } #[test] @@ -20,7 +18,10 @@ fn test_monochromatic_triangle_creation() { // K4 has 4 triangles assert_eq!(problem.triangles().len(), 4); // One binary variable per edge - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!(problem.graph().num_vertices(), 4); } @@ -67,7 +68,7 @@ fn test_monochromatic_triangle_evaluate_wrong_length() { #[test] fn test_monochromatic_triangle_triangle_free_graph() { // A path graph 0-1-2 has no triangles, so any coloring is valid. - let problem = MonochromaticTriangle::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = MonochromaticTriangle::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); assert_eq!(problem.triangles().len(), 0); assert!(problem.evaluate(&vec![false, false]).unwrap()); assert!(problem.evaluate(&vec![true, true]).unwrap()); @@ -92,7 +93,7 @@ fn test_monochromatic_triangle_brute_force_k6_no_solution() { edges.push((u, v)); } } - let problem = MonochromaticTriangle::new(SimpleGraph::new(6, edges)); + let problem = MonochromaticTriangle::new(SimpleGraph::new(6, edges).unwrap()); assert_eq!(problem.num_edges(), 15); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); @@ -107,7 +108,7 @@ fn test_monochromatic_triangle_brute_force_k5_has_solution() { edges.push((u, v)); } } - let problem = MonochromaticTriangle::new(SimpleGraph::new(5, edges)); + let problem = MonochromaticTriangle::new(SimpleGraph::new(5, edges).unwrap()); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); assert!(solution.is_some()); diff --git a/src/unit_tests/models/graph/multiple_choice_branching.rs b/src/unit_tests/models/graph/multiple_choice_branching.rs index c4054593d..078f1f56b 100644 --- a/src/unit_tests/models/graph/multiple_choice_branching.rs +++ b/src/unit_tests/models/graph/multiple_choice_branching.rs @@ -35,7 +35,8 @@ fn yes_instance() -> MultipleChoiceBranching { (4, 5), (2, 4), ], - ), + ) + .unwrap(), vec![3, 2, 4, 1, 2, 3, 1, 3], vec![vec![0, 1], vec![2, 3], vec![4, 7], vec![5, 6]], 10, @@ -44,7 +45,7 @@ fn yes_instance() -> MultipleChoiceBranching { fn no_instance() -> MultipleChoiceBranching { MultipleChoiceBranching::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![2, 2], vec![vec![0], vec![1]], 5, @@ -58,7 +59,10 @@ fn test_multiple_choice_branching_creation_and_accessors() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_arcs(), 8); assert_eq!(problem.num_partition_groups(), 4); - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); assert_eq!(problem.graph().arcs().len(), 8); assert_eq!(problem.weights(), &[3, 2, 4, 1, 2, 3, 1, 3]); assert_eq!( @@ -76,7 +80,7 @@ fn test_multiple_choice_branching_creation_and_accessors() { fn test_multiple_choice_branching_rejects_weight_length_mismatch() { let result = std::panic::catch_unwind(|| { MultipleChoiceBranching::new( - DirectedGraph::new(2, vec![(0, 1)]), + DirectedGraph::new(2, vec![(0, 1)]).unwrap(), vec![1, 2], vec![vec![0]], 1, @@ -89,7 +93,7 @@ fn test_multiple_choice_branching_rejects_weight_length_mismatch() { fn test_multiple_choice_branching_partition_validation_out_of_range() { let result = std::panic::catch_unwind(|| { MultipleChoiceBranching::new( - DirectedGraph::new(2, vec![(0, 1)]), + DirectedGraph::new(2, vec![(0, 1)]).unwrap(), vec![1], vec![vec![1]], 1, @@ -102,7 +106,7 @@ fn test_multiple_choice_branching_partition_validation_out_of_range() { fn test_multiple_choice_branching_partition_validation_overlap() { let result = std::panic::catch_unwind(|| { MultipleChoiceBranching::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1, 1], vec![vec![0, 1], vec![1]], 1, @@ -115,7 +119,7 @@ fn test_multiple_choice_branching_partition_validation_overlap() { fn test_multiple_choice_branching_partition_validation_missing_arc() { let result = std::panic::catch_unwind(|| { MultipleChoiceBranching::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1, 1], vec![vec![0]], 1, @@ -165,7 +169,7 @@ fn test_multiple_choice_branching_rejects_non_binary_config_value() { #[test] fn test_multiple_choice_branching_rejects_indegree_violation() { let problem = MultipleChoiceBranching::new( - DirectedGraph::new(3, vec![(0, 2), (1, 2)]), + DirectedGraph::new(3, vec![(0, 2), (1, 2)]).unwrap(), vec![2, 2], vec![vec![0], vec![1]], 1, @@ -176,7 +180,7 @@ fn test_multiple_choice_branching_rejects_indegree_violation() { #[test] fn test_multiple_choice_branching_rejects_cycle_violation() { let problem = MultipleChoiceBranching::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(), vec![1, 1, 1], vec![vec![0], vec![1], vec![2]], 1, @@ -264,7 +268,7 @@ fn test_multiple_choice_branching_deserialize_rejects_invalid_partition() { fn test_multiple_choice_branching_set_weights_rejects_wrong_length() { let result = std::panic::catch_unwind(|| { let mut problem = MultipleChoiceBranching::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1, 1], vec![vec![0], vec![1]], 1, @@ -277,5 +281,5 @@ fn test_multiple_choice_branching_set_weights_rejects_wrong_length() { #[test] fn test_multiple_choice_branching_num_variables() { let problem = yes_instance(); - assert_eq!(problem.num_variables(), 8); + assert_eq!(problem.num_variables().unwrap(), 8); } 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 f8b910cf7..b00ccd75e 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,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_preserves_isolated_vertices() { @@ -18,8 +17,8 @@ use crate::traits::Problem; use crate::types::Min; fn cycle_instance() -> MultipleCopyFileAllocation { - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)]); - MultipleCopyFileAllocation::new(graph, vec![10; 6], vec![1; 6]) + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)]).unwrap(); + MultipleCopyFileAllocation::new(graph, vec![10; 6], vec![1; 6]).unwrap() } #[test] @@ -31,7 +30,10 @@ fn test_multiple_copy_file_allocation_creation() { assert_eq!(problem.num_edges(), 6); assert_eq!(problem.usage(), &[10; 6]); assert_eq!(problem.storage(), &[1; 6]); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert!(MultipleCopyFileAllocation::variant().is_empty()); } @@ -48,10 +50,11 @@ fn test_multiple_copy_file_allocation_total_cost_and_validity() { #[test] fn test_multiple_copy_file_allocation_reports_cost_overflow() { let problem = MultipleCopyFileAllocation::new( - SimpleGraph::new(2, vec![(0, 1)]), + SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![0, 0], vec![i64::MAX, 1], - ); + ) + .unwrap(); assert!(matches!( problem.evaluate(&vec![true, true]), Err(crate::traits::EvaluationError::IntegerOverflow(_)) @@ -61,10 +64,11 @@ fn test_multiple_copy_file_allocation_reports_cost_overflow() { #[test] fn test_multiple_copy_file_allocation_uses_per_vertex_costs() { let problem = MultipleCopyFileAllocation::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1, 10, 100, 1000], vec![3, 5, 7, 11], - ); + ) + .unwrap(); let config = vec![true, false, true, false]; assert_eq!(problem.total_cost(&config).unwrap(), Some(1020)); @@ -99,8 +103,8 @@ fn test_multiple_copy_file_allocation_invalid_configs() { #[test] fn test_multiple_copy_file_allocation_unreachable_component_is_invalid() { - let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]); - let problem = MultipleCopyFileAllocation::new(graph, vec![5; 4], vec![1; 4]); + let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(); + let problem = MultipleCopyFileAllocation::new(graph, vec![5; 4], vec![1; 4]).unwrap(); let config = vec![true, false, false, false]; assert_eq!(problem.total_cost(&config).unwrap(), None); diff --git a/src/unit_tests/models/graph/optimal_linear_arrangement.rs b/src/unit_tests/models/graph/optimal_linear_arrangement.rs index cd433e63d..1a93ca1cf 100644 --- a/src/unit_tests/models/graph/optimal_linear_arrangement.rs +++ b/src/unit_tests/models/graph/optimal_linear_arrangement.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Min; @@ -10,13 +9,14 @@ fn issue_example() -> OptimalLinearArrangement { let graph = SimpleGraph::new( 6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 3), (2, 5)], - ); + ) + .unwrap(); OptimalLinearArrangement::new(graph) } /// Path graph: 0-1-2-3-4-5 fn path_example() -> OptimalLinearArrangement { - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]); + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]).unwrap(); OptimalLinearArrangement::new(graph) } @@ -25,7 +25,10 @@ fn test_optimallineararrangement_basic() { let problem = issue_example(); // Check dims: 6 variables, each with domain size 6 - assert_eq!(problem.dimensions(), vec![6, 6, 6, 6, 6, 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6, 6, 6, 6, 6, 6] + ); // Identity arrangement: f(i) = i // Cost: |0-1| + |1-2| + |2-3| + |3-4| + |4-5| + |0-3| + |2-5| = 1+1+1+1+1+3+3 = 11 @@ -96,7 +99,7 @@ fn test_optimallineararrangement_serialization() { fn test_optimallineararrangement_solver() { // Small graph: triangle // Any permutation of 3 vertices on a triangle has cost 4 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); let problem = OptimalLinearArrangement::new(graph); let solver = BruteForce::new(); @@ -109,7 +112,7 @@ fn test_optimallineararrangement_solver() { #[test] fn test_optimallineararrangement_solver_aggregate() { // Triangle: minimum arrangement cost is 4 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); let problem = OptimalLinearArrangement::new(graph); let solver = BruteForce::new(); @@ -121,7 +124,7 @@ fn test_optimallineararrangement_solver_aggregate() { #[test] fn test_optimallineararrangement_empty_graph() { // No edges: any permutation has cost 0 - let graph = SimpleGraph::new(3, vec![]); + let graph = SimpleGraph::new(3, vec![]).unwrap(); let problem = OptimalLinearArrangement::new(graph); let solver = BruteForce::new(); @@ -140,10 +143,13 @@ fn test_optimallineararrangement_empty_graph() { #[test] fn test_optimallineararrangement_single_vertex() { - let graph = SimpleGraph::new(1, vec![]); + let graph = SimpleGraph::new(1, vec![]).unwrap(); let problem = OptimalLinearArrangement::new(graph); - assert_eq!(problem.dimensions(), vec![1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1] + ); assert_eq!(problem.evaluate(&vec![0]).unwrap(), Min(Some(0))); assert_eq!(problem.total_edge_length(&[0]).unwrap(), Some(0)); } @@ -174,7 +180,7 @@ fn test_optimallineararrangement_problem_name() { #[test] fn test_optimallineararrangement_two_vertices() { // Single edge: 0-1 - let graph = SimpleGraph::new(2, vec![(0, 1)]); + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); let problem = OptimalLinearArrangement::new(graph); // Both permutations [0,1] and [1,0] have cost 1 @@ -187,7 +193,7 @@ fn test_optimallineararrangement_two_vertices() { #[test] fn test_optimallineararrangement_permutation_matters() { // Path 0-1-2-3 - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = OptimalLinearArrangement::new(graph); // Identity: cost = 1+1+1 = 3 @@ -207,7 +213,7 @@ fn test_optimallineararrangement_permutation_matters() { #[test] fn test_optimallineararrangement_is_valid_solution() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = OptimalLinearArrangement::new(graph); // Valid permutation @@ -225,7 +231,7 @@ fn test_optimallineararrangement_is_valid_solution() { fn test_optimallineararrangement_complete_graph_k4() { // K4: all 6 edges present // For K4, any linear arrangement has cost 1+2+3+1+2+1 = 10 - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); let problem = OptimalLinearArrangement::new(graph); let solver = BruteForce::new(); 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 c39b3895a..be3371e11 100644 --- a/src/unit_tests/models/graph/partial_feedback_edge_set.rs +++ b/src/unit_tests/models/graph/partial_feedback_edge_set.rs @@ -8,7 +8,7 @@ fn create_spec_constructs_model() { "max_cycle_length" ); let problem = PartialFeedbackEdgeSet::try_from(PartialFeedbackEdgeSetCreateSpec { - graph: SimpleGraph::new(2, vec![(0, 1)]), + graph: SimpleGraph::new(2, vec![(0, 1)]).unwrap(), budget: 1, max_cycle_length: 3, }) @@ -34,6 +34,7 @@ fn issue_graph() -> SimpleGraph { (0, 3), ], ) + .unwrap() } fn yes_instance() -> PartialFeedbackEdgeSet { @@ -66,8 +67,11 @@ fn test_partial_feedback_edge_set_creation() { assert_eq!(problem.max_cycle_length(), 4); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 9); - assert_eq!(problem.num_variables(), 9); - assert_eq!(problem.dimensions(), vec![2; 9]); + assert_eq!(problem.num_variables().unwrap(), 9); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 9] + ); } #[test] diff --git a/src/unit_tests/models/graph/partition_into_cliques.rs b/src/unit_tests/models/graph/partition_into_cliques.rs index 2ee0b8fe3..95cc83495 100644 --- a/src/unit_tests/models/graph/partition_into_cliques.rs +++ b/src/unit_tests/models/graph/partition_into_cliques.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -20,9 +19,11 @@ fn two_triangle_instance() -> PartitionIntoCliques { (1, 4), (2, 5), ], - ), + ) + .unwrap(), 3, ) + .unwrap() } #[test] @@ -31,7 +32,10 @@ fn test_partition_into_cliques_creation() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 9); assert_eq!(problem.num_cliques(), 3); - assert_eq!(problem.dimensions(), vec![3; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 6] + ); assert_eq!(problem.graph().num_vertices(), 6); } @@ -81,9 +85,10 @@ fn test_partition_into_cliques_evaluate_out_of_range_group() { fn test_partition_into_cliques_brute_force_finds_solution() { // Complete graph K4, K=2: can partition into two cliques let problem = PartitionIntoCliques::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), 2, - ); + ) + .unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); assert!(solution.is_some()); @@ -93,7 +98,8 @@ fn test_partition_into_cliques_brute_force_finds_solution() { #[test] fn test_partition_into_cliques_brute_force_no_solution() { // Path 0-1-2, K=1: {0,1,2} not a clique (missing edge 0-2) - let problem = PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 1); + let problem = + PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 1).unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); } @@ -101,7 +107,11 @@ fn test_partition_into_cliques_brute_force_no_solution() { #[test] fn test_partition_into_cliques_brute_force_all_valid() { // Complete graph K3, K=3: every assignment is valid - let problem = PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]), 3); + let problem = PartitionIntoCliques::new( + SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(), + 3, + ) + .unwrap(); let solutions = BruteForce::new().find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); for sol in &solutions { @@ -120,13 +130,11 @@ fn test_partition_into_cliques_serialization() { } #[test] -#[should_panic(expected = "num_cliques must be at least 1")] fn test_partition_into_cliques_rejects_zero() { - let _ = PartitionIntoCliques::new(SimpleGraph::new(2, vec![(0, 1)]), 0); + assert!(PartitionIntoCliques::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 0).is_err()); } #[test] -#[should_panic(expected = "num_cliques must be at most num_vertices")] fn test_partition_into_cliques_rejects_too_many() { - let _ = PartitionIntoCliques::new(SimpleGraph::new(2, vec![(0, 1)]), 3); + assert!(PartitionIntoCliques::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 3).is_err()); } diff --git a/src/unit_tests/models/graph/partition_into_forests.rs b/src/unit_tests/models/graph/partition_into_forests.rs index de56c11ba..1289c8374 100644 --- a/src/unit_tests/models/graph/partition_into_forests.rs +++ b/src/unit_tests/models/graph/partition_into_forests.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -11,9 +10,11 @@ fn two_triangle_instance() -> PartitionIntoForests { SimpleGraph::new( 6, vec![(0, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 5), (5, 3)], - ), + ) + .unwrap(), 2, ) + .unwrap() } #[test] @@ -22,7 +23,10 @@ fn test_partition_into_forests_creation() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 7); assert_eq!(problem.num_forests(), 2); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!(problem.graph().num_vertices(), 6); } @@ -41,9 +45,10 @@ fn test_partition_into_forests_evaluate_positive() { fn test_partition_into_forests_evaluate_negative_k1() { // K=1: must put all vertices in one class; two triangles create cycles let problem = PartitionIntoForests::new( - SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3)]), + SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3)]).unwrap(), 1, - ); + ) + .unwrap(); // Any single-class assignment must include a triangle → cycle assert!(!problem.evaluate(&vec![0, 0, 0, 0, 0, 0]).unwrap()); @@ -83,8 +88,11 @@ fn test_partition_into_forests_evaluate_out_of_range_class() { #[test] fn test_partition_into_forests_brute_force_finds_solution() { // Small instance: 4-cycle (no triangle), K=2 should work easily - let problem = - PartitionIntoForests::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]), 2); + let problem = PartitionIntoForests::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(), + 2, + ) + .unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); assert!(solution.is_some()); @@ -94,7 +102,11 @@ fn test_partition_into_forests_brute_force_finds_solution() { #[test] fn test_partition_into_forests_brute_force_no_solution() { // Single triangle, K=1: impossible - let problem = PartitionIntoForests::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]), 1); + let problem = PartitionIntoForests::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(), + 1, + ) + .unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); } @@ -102,7 +114,8 @@ fn test_partition_into_forests_brute_force_no_solution() { #[test] fn test_partition_into_forests_brute_force_all_valid() { // Small acyclic graph (path 0-1-2), K=1: every assignment is valid - let problem = PartitionIntoForests::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 1); + let problem = + PartitionIntoForests::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 1).unwrap(); let solutions = BruteForce::new().find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); for sol in &solutions { @@ -121,7 +134,6 @@ fn test_partition_into_forests_serialization() { } #[test] -#[should_panic(expected = "num_forests must be at least 1")] fn test_partition_into_forests_rejects_zero_forests() { - let _ = PartitionIntoForests::new(SimpleGraph::new(2, vec![(0, 1)]), 0); + assert!(PartitionIntoForests::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 0).is_err()); } 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 a3421104a..246da4c67 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,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -21,13 +20,17 @@ fn test_partition_into_paths_basic() { (3, 6), (5, 8), ], - ); - let problem = PartitionIntoPathsOfLength2::new(graph); + ) + .unwrap(); + let problem = PartitionIntoPathsOfLength2::new(graph).unwrap(); assert_eq!(problem.num_vertices(), 9); assert_eq!(problem.num_edges(), 10); assert_eq!(problem.num_groups(), 3); - assert_eq!(problem.dimensions(), vec![3; 9]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 9] + ); // Valid partition: {0,1,2}, {3,4,5}, {6,7,8} // Config: vertex i -> group i/3 @@ -45,8 +48,8 @@ fn test_partition_into_paths_basic() { fn test_partition_into_paths_no_solution() { // 6-vertex graph where no valid partition exists // Edges: {0,1}, {2,3}, {0,4}, {1,5} - let graph = SimpleGraph::new(6, vec![(0, 1), (2, 3), (0, 4), (1, 5)]); - let problem = PartitionIntoPathsOfLength2::new(graph); + let graph = SimpleGraph::new(6, vec![(0, 1), (2, 3), (0, 4), (1, 5)]).unwrap(); + let problem = PartitionIntoPathsOfLength2::new(graph).unwrap(); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_groups(), 2); @@ -59,8 +62,8 @@ fn test_partition_into_paths_no_solution() { #[test] fn test_partition_into_paths_solver() { // Simple 6-vertex graph with obvious partition: 0-1-2 and 3-4-5 - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]); - let problem = PartitionIntoPathsOfLength2::new(graph); + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]).unwrap(); + let problem = PartitionIntoPathsOfLength2::new(graph).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -74,8 +77,8 @@ fn test_partition_into_paths_solver() { #[test] fn test_partition_into_paths_invalid_group_size() { // 6-vertex path: 0-1-2-3-4-5 - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]); - let problem = PartitionIntoPathsOfLength2::new(graph); + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]).unwrap(); + let problem = PartitionIntoPathsOfLength2::new(graph).unwrap(); // Config where group 0 has 4 vertices and group 1 has 2 vertices let bad_config = vec![0, 0, 0, 0, 1, 1]; @@ -85,8 +88,8 @@ fn test_partition_into_paths_invalid_group_size() { #[test] fn test_partition_into_paths_insufficient_edges() { // 6 vertices, only 2 edges — not enough for any group to have 2 edges - let graph = SimpleGraph::new(6, vec![(0, 1), (3, 4)]); - let problem = PartitionIntoPathsOfLength2::new(graph); + let graph = SimpleGraph::new(6, vec![(0, 1), (3, 4)]).unwrap(); + let problem = PartitionIntoPathsOfLength2::new(graph).unwrap(); // Even a well-sized partition fails because groups lack edges let config = vec![0, 0, 0, 1, 1, 1]; @@ -97,8 +100,8 @@ fn test_partition_into_paths_insufficient_edges() { #[test] fn test_partition_into_paths_triangle() { // Triangle group: 3 vertices, 3 edges — also valid (>= 2 edges) - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let problem = PartitionIntoPathsOfLength2::new(graph); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let problem = PartitionIntoPathsOfLength2::new(graph).unwrap(); // Single group with all 3 vertices forming a triangle let config = vec![0, 0, 0]; @@ -107,8 +110,8 @@ fn test_partition_into_paths_triangle() { #[test] fn test_partition_into_paths_serialization() { - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]); - let problem = PartitionIntoPathsOfLength2::new(graph); + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]).unwrap(); + let problem = PartitionIntoPathsOfLength2::new(graph).unwrap(); let json = serde_json::to_string(&problem).unwrap(); let deserialized: PartitionIntoPathsOfLength2 = @@ -127,16 +130,15 @@ fn test_partition_into_paths_serialization() { } #[test] -#[should_panic(expected = "must be divisible by 3")] fn test_partition_into_paths_invalid_vertex_count() { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); - let _problem = PartitionIntoPathsOfLength2::new(graph); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(); + assert!(PartitionIntoPathsOfLength2::new(graph).is_err()); } #[test] fn test_partition_into_paths_parameter_getters() { - let graph = SimpleGraph::new(9, vec![(0, 1), (1, 2), (3, 4), (4, 5), (6, 7), (7, 8)]); - let problem = PartitionIntoPathsOfLength2::new(graph); + let graph = SimpleGraph::new(9, vec![(0, 1), (1, 2), (3, 4), (4, 5), (6, 7), (7, 8)]).unwrap(); + let problem = PartitionIntoPathsOfLength2::new(graph).unwrap(); assert_eq!(problem.num_vertices(), 9); assert_eq!(problem.num_edges(), 6); assert_eq!(problem.num_groups(), 3); @@ -144,8 +146,8 @@ fn test_partition_into_paths_parameter_getters() { #[test] fn test_partition_into_paths_out_of_range_group() { - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]); - let problem = PartitionIntoPathsOfLength2::new(graph); + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]).unwrap(); + let problem = PartitionIntoPathsOfLength2::new(graph).unwrap(); // Group index out of range (q=2, so valid groups are 0 and 1) let config = vec![0, 0, 0, 2, 2, 2]; @@ -157,8 +159,8 @@ fn test_partition_into_paths_out_of_range_group() { #[test] fn test_partition_into_paths_is_valid_partition() { - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]); - let problem = PartitionIntoPathsOfLength2::new(graph); + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]).unwrap(); + let problem = PartitionIntoPathsOfLength2::new(graph).unwrap(); assert!(problem.is_valid_partition(&[0, 0, 0, 1, 1, 1])); assert!(!problem.is_valid_partition(&[0, 0, 1, 1, 1, 1])); // Wrong group sizes diff --git a/src/unit_tests/models/graph/partition_into_perfect_matchings.rs b/src/unit_tests/models/graph/partition_into_perfect_matchings.rs index 8ea2f64a2..9fd4f61e0 100644 --- a/src/unit_tests/models/graph/partition_into_perfect_matchings.rs +++ b/src/unit_tests/models/graph/partition_into_perfect_matchings.rs @@ -1,12 +1,15 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; fn four_vertex_instance() -> PartitionIntoPerfectMatchings { // 4 vertices with edges: (0,1),(2,3),(0,2),(1,3) - PartitionIntoPerfectMatchings::new(SimpleGraph::new(4, vec![(0, 1), (2, 3), (0, 2), (1, 3)]), 2) + PartitionIntoPerfectMatchings::new( + SimpleGraph::new(4, vec![(0, 1), (2, 3), (0, 2), (1, 3)]).unwrap(), + 2, + ) + .unwrap() } #[test] @@ -15,7 +18,10 @@ fn test_partition_into_perfect_matchings_creation() { assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 4); assert_eq!(problem.num_matchings(), 2); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!(problem.graph().num_vertices(), 4); } @@ -85,7 +91,9 @@ fn test_partition_into_perfect_matchings_brute_force_finds_solution() { fn test_partition_into_perfect_matchings_brute_force_no_solution() { // Path 0-1-2: no perfect matching partition possible with K=1 // Group {0,1,2} has 3 vertices (odd) so cannot be a perfect matching - let problem = PartitionIntoPerfectMatchings::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 1); + let problem = + PartitionIntoPerfectMatchings::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 1) + .unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); } @@ -93,7 +101,8 @@ fn test_partition_into_perfect_matchings_brute_force_no_solution() { #[test] fn test_partition_into_perfect_matchings_brute_force_all_valid() { // 2 vertices with edge (0,1), K=2: group {0,1} is a perfect matching - let problem = PartitionIntoPerfectMatchings::new(SimpleGraph::new(2, vec![(0, 1)]), 2); + let problem = + PartitionIntoPerfectMatchings::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 2).unwrap(); let solutions = BruteForce::new().find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); for sol in &solutions { @@ -113,13 +122,15 @@ fn test_partition_into_perfect_matchings_serialization() { } #[test] -#[should_panic(expected = "num_matchings must be at least 1")] fn test_partition_into_perfect_matchings_rejects_zero() { - let _ = PartitionIntoPerfectMatchings::new(SimpleGraph::new(2, vec![(0, 1)]), 0); + assert!( + PartitionIntoPerfectMatchings::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 0).is_err() + ); } #[test] -#[should_panic(expected = "num_matchings must be at most num_vertices")] fn test_partition_into_perfect_matchings_rejects_too_many() { - let _ = PartitionIntoPerfectMatchings::new(SimpleGraph::new(2, vec![(0, 1)]), 3); + assert!( + PartitionIntoPerfectMatchings::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 3).is_err() + ); } diff --git a/src/unit_tests/models/graph/partition_into_triangles.rs b/src/unit_tests/models/graph/partition_into_triangles.rs index 42fb8c9c3..f6de28d9b 100644 --- a/src/unit_tests/models/graph/partition_into_triangles.rs +++ b/src/unit_tests/models/graph/partition_into_triangles.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; #[test] @@ -20,12 +19,16 @@ fn test_partitionintotriangles_basic() { (7, 8), (6, 8), ], - ); - let problem = PartitionIntoTriangles::new(graph); + ) + .unwrap(); + let problem = PartitionIntoTriangles::new(graph).unwrap(); assert_eq!(problem.num_vertices(), 9); assert_eq!(problem.num_edges(), 9); - assert_eq!(problem.dimensions(), vec![3; 9]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 9] + ); // Valid partition: vertices 0,1,2 in group 0; 3,4,5 in group 1; 6,7,8 in group 2 assert!(problem.evaluate(&vec![0, 0, 0, 1, 1, 1, 2, 2, 2]).unwrap()); @@ -40,11 +43,14 @@ fn test_partitionintotriangles_basic() { #[test] fn test_partitionintotriangles_no_solution() { // 6-vertex NO instance: path graph has no triangles at all - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]); - let problem = PartitionIntoTriangles::new(graph); + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]).unwrap(); + let problem = PartitionIntoTriangles::new(graph).unwrap(); assert_eq!(problem.num_vertices(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); // No valid partition exists since there are no triangles let solver = BruteForce::new(); @@ -57,8 +63,8 @@ fn test_partitionintotriangles_solver() { use crate::traits::Problem; // Single triangle - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let problem = PartitionIntoTriangles::new(graph); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let problem = PartitionIntoTriangles::new(graph).unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); @@ -76,8 +82,8 @@ fn test_partitionintotriangles_solver() { #[test] fn test_partitionintotriangles_serialization() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let problem = PartitionIntoTriangles::new(graph); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let problem = PartitionIntoTriangles::new(graph).unwrap(); let json = serde_json::to_string(&problem).unwrap(); let deserialized: PartitionIntoTriangles = serde_json::from_str(&json).unwrap(); @@ -87,18 +93,17 @@ fn test_partitionintotriangles_serialization() { } #[test] -#[should_panic(expected = "must be divisible by 3")] fn test_partitionintotriangles_invalid_vertex_count() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); - let _ = PartitionIntoTriangles::new(graph); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); + assert!(PartitionIntoTriangles::new(graph).is_err()); } #[test] fn test_partitionintotriangles_config_out_of_range() { use crate::traits::Problem; - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let problem = PartitionIntoTriangles::new(graph); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let problem = PartitionIntoTriangles::new(graph).unwrap(); // q = 1, so only group 0 is valid; group 1 is out of range assert!(matches!( @@ -111,8 +116,8 @@ fn test_partitionintotriangles_config_out_of_range() { fn test_partitionintotriangles_wrong_config_length() { use crate::traits::Problem; - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let problem = PartitionIntoTriangles::new(graph); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let problem = PartitionIntoTriangles::new(graph).unwrap(); assert!(matches!( problem.evaluate(&vec![0, 0]), @@ -126,8 +131,8 @@ fn test_partitionintotriangles_wrong_config_length() { #[test] fn test_partitionintotriangles_parameter_getters() { - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5)]); - let problem = PartitionIntoTriangles::new(graph); + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5)]).unwrap(); + let problem = PartitionIntoTriangles::new(graph).unwrap(); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 6); } @@ -139,8 +144,9 @@ fn test_partitionintotriangles_paper_example() { let graph = SimpleGraph::new( 6, vec![(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (0, 3)], - ); - let problem = PartitionIntoTriangles::new(graph); + ) + .unwrap(); + let problem = PartitionIntoTriangles::new(graph).unwrap(); // Valid partition: {0,1,2} in group 0, {3,4,5} in group 1 assert!(problem.evaluate(&vec![0, 0, 0, 1, 1, 1]).unwrap()); 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 af1cf3617..1802afd8d 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,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_capacities_and_validates_paths() { @@ -34,7 +33,8 @@ fn yes_instance() -> PathConstrainedNetworkFlow { (5, 7), (6, 7), ], - ); + ) + .unwrap(); PathConstrainedNetworkFlow::new( graph, @@ -76,7 +76,10 @@ fn test_path_constrained_network_flow_creation() { #[test] fn test_path_constrained_network_flow_dims_use_path_bottlenecks() { let problem = yes_instance(); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2, 2] + ); } #[test] @@ -125,7 +128,7 @@ fn test_path_constrained_network_flow_serialization() { #[test] fn test_path_constrained_network_flow_rejects_non_contiguous_path() { - let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let result = std::panic::catch_unwind(|| { PathConstrainedNetworkFlow::new(graph, vec![1, 1, 1], 0, 3, vec![vec![0, 2]], 1) }); @@ -134,7 +137,7 @@ fn test_path_constrained_network_flow_rejects_non_contiguous_path() { #[test] fn test_path_constrained_network_flow_rejects_empty_path() { - let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let result = std::panic::catch_unwind(|| { PathConstrainedNetworkFlow::new(graph, vec![1, 1, 1], 0, 3, vec![vec![]], 1) }); @@ -143,7 +146,7 @@ fn test_path_constrained_network_flow_rejects_empty_path() { #[test] fn test_path_constrained_network_flow_rejects_path_not_ending_at_sink() { - let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let result = std::panic::catch_unwind(|| { PathConstrainedNetworkFlow::new(graph, vec![1, 1, 1], 0, 3, vec![vec![0, 1]], 1) }); @@ -153,7 +156,7 @@ fn test_path_constrained_network_flow_rejects_path_not_ending_at_sink() { #[test] fn test_path_constrained_network_flow_rejects_path_with_repeated_vertex() { // Graph: 0->1, 1->2, 2->1, 1->3 (arcs 0,1,2,3) - let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 1), (1, 3)]); + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 1), (1, 3)]).unwrap(); let result = std::panic::catch_unwind(|| { // Path [0, 1, 2, 3]: 0->1->2->1->3 revisits vertex 1 PathConstrainedNetworkFlow::new(graph, vec![1, 1, 1, 1], 0, 3, vec![vec![0, 1, 2, 3]], 1) diff --git a/src/unit_tests/models/graph/prize_collecting_steiner_forest.rs b/src/unit_tests/models/graph/prize_collecting_steiner_forest.rs index e5a801e5a..260b630da 100644 --- a/src/unit_tests/models/graph/prize_collecting_steiner_forest.rs +++ b/src/unit_tests/models/graph/prize_collecting_steiner_forest.rs @@ -10,7 +10,7 @@ use crate::types::Min; /// beta = 1, omega = 2. fn canonical_problem() -> PrizeCollectingSteinerForest { PrizeCollectingSteinerForest::::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![5, 2, 5], vec![1, 6], 1, @@ -29,8 +29,11 @@ fn test_prize_collecting_steiner_forest_creation() { assert_eq!(*problem.beta(), 1); assert_eq!(*problem.omega(), 2); // n + m = 3 + 2 = 5 binary variables. - assert_eq!(problem.dimensions(), vec![2; 5]); - assert_eq!(problem.num_variables(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); + assert_eq!(problem.num_variables().unwrap(), 5); assert!(problem.graph().has_edge(0, 1)); } @@ -99,7 +102,7 @@ fn test_prize_collecting_steiner_forest_evaluate_cycle_infeasible() { // Triangle 0-1, 1-2, 0-2 with all three vertices and all three edges // selected forms a cycle, which is not a forest -> infeasible. let problem = PrizeCollectingSteinerForest::::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1, 1, 1], vec![1, 1, 1], 1, @@ -151,7 +154,7 @@ fn test_prize_collecting_steiner_forest_serialization_roundtrip() { fn test_prize_collecting_steiner_forest_f64_variant() { // Same canonical instance with f64 weights exercises the second registered variant. let problem = PrizeCollectingSteinerForest::::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![5.0, 2.0, 5.0], vec![1.0, 6.0], 1.0, @@ -175,7 +178,7 @@ fn test_prize_collecting_steiner_forest_f64_variant() { #[test] fn test_prize_collecting_steiner_forest_rejects_vertex_prizes_length_mismatch() { let error = PrizeCollectingSteinerForest::::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![5, 2], // length 2 != 3 vertices vec![1, 6], 1, @@ -192,7 +195,7 @@ fn test_prize_collecting_steiner_forest_rejects_vertex_prizes_length_mismatch() #[test] fn test_prize_collecting_steiner_forest_rejects_edge_costs_length_mismatch() { let error = PrizeCollectingSteinerForest::::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![5, 2, 5], vec![1, 6, 2], // length 3 != 2 edges 1, @@ -209,7 +212,7 @@ fn test_prize_collecting_steiner_forest_rejects_edge_costs_length_mismatch() { #[test] fn test_prize_collecting_steiner_forest_rejects_non_finite_weight() { assert!(PrizeCollectingSteinerForest::::new( - SimpleGraph::new(1, vec![]), + SimpleGraph::new(1, vec![]).unwrap(), vec![f64::NAN], vec![], 1.0, @@ -246,3 +249,25 @@ fn create_specs_default_prizes_and_costs_to_one() { assert!(!PrizeCollectingSteinerForestI64CreateSpec::inputs()[2].required); assert!(!PrizeCollectingSteinerForestI64CreateSpec::inputs()[3].required); } + +#[test] +fn nonnegative_domain_is_shared_by_construction_and_serde() { + for (prizes, costs, beta, omega) in [ + (vec![-1, 0], vec![0], 1, 1), + (vec![0, 0], vec![-1], 1, 1), + (vec![0, 0], vec![0], -1, 1), + (vec![0, 0], vec![0], 1, -1), + ] { + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); + let data = serde_json::json!({"graph": graph, "vertex_prizes": prizes, + "edge_costs": costs, "beta": beta, "omega": omega}); + assert!( + serde_json::from_value::>(data.clone()) + .is_err() + ); + assert!( + serde_json::from_value::>(data).is_err() + ); + assert!(PrizeCollectingSteinerForest::new(graph, prizes, costs, beta, omega).is_err()); + } +} diff --git a/src/unit_tests/models/graph/rooted_tree_arrangement.rs b/src/unit_tests/models/graph/rooted_tree_arrangement.rs index a34c88abf..6b1253aa0 100644 --- a/src/unit_tests/models/graph/rooted_tree_arrangement.rs +++ b/src/unit_tests/models/graph/rooted_tree_arrangement.rs @@ -1,11 +1,10 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; fn issue_example() -> RootedTreeArrangement { - let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 2), (2, 3), (3, 4)]); + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 2), (2, 3), (3, 4)]).unwrap(); RootedTreeArrangement::new(graph, 7) } @@ -21,7 +20,10 @@ fn test_rootedtreearrangement_basic_yes_example() { assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 5); assert_eq!(problem.bound(), 7); - assert_eq!(problem.dimensions(), vec![5; 10]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 10] + ); assert!(problem.evaluate(&config).unwrap()); assert_eq!(problem.total_edge_stretch(&config).unwrap(), Some(6)); } @@ -63,7 +65,7 @@ fn test_rootedtreearrangement_rejects_invalid_bijections() { #[test] fn test_rootedtreearrangement_rejects_noncomparable_edges() { - let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 2), (2, 3), (3, 4)]); + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 2), (2, 3), (3, 4)]).unwrap(); let problem = RootedTreeArrangement::new(graph, 99); // Tree: 0 is root, 1 and 2 are siblings, 3 and 4 descend from 2. @@ -85,7 +87,7 @@ fn test_rootedtreearrangement_enforces_bound() { #[test] fn test_rootedtreearrangement_solver_and_serialization() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = RootedTreeArrangement::new(graph, 2); let solver = BruteForce::new(); diff --git a/src/unit_tests/models/graph/rural_postman.rs b/src/unit_tests/models/graph/rural_postman.rs index aec2d3a5d..e9c4f4e8f 100644 --- a/src/unit_tests/models/graph/rural_postman.rs +++ b/src/unit_tests/models/graph/rural_postman.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Min; @@ -21,19 +20,20 @@ fn hexagon_rpp() -> RuralPostman { (0, 3), (1, 4), ], - ); + ) + .unwrap(); let edge_lengths = vec![1, 1, 1, 1, 1, 1, 2, 2]; // Required edges: {0,1}=idx 0, {2,3}=idx 2, {4,5}=idx 4 let required_edges = vec![0, 2, 4]; - RuralPostman::new(graph, edge_lengths, required_edges) + RuralPostman::new(graph, edge_lengths, required_edges).unwrap() } /// Instance 3 from issue: C4 cycle, all edges required (Chinese Postman) fn chinese_postman_rpp() -> RuralPostman { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(); let edge_lengths = vec![1, 1, 1, 1]; let required_edges = vec![0, 1, 2, 3]; - RuralPostman::new(graph, edge_lengths, required_edges) + RuralPostman::new(graph, edge_lengths, required_edges).unwrap() } #[test] @@ -42,8 +42,16 @@ fn test_rural_postman_creation() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 8); assert_eq!(problem.num_required_edges(), 3); - assert_eq!(problem.dimensions().len(), 8); - assert!(problem.dimensions().iter().all(|&d| d == 3)); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 8 + ); + assert!(crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .iter() + .all(|&d| d == 3)); } #[test] @@ -91,10 +99,10 @@ fn test_rural_postman_chinese_postman_case() { #[test] fn test_rural_postman_no_edges_no_required() { // No required edges — selecting no edges is valid (empty circuit, cost 0) - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); let edge_lengths = vec![1, 1, 1]; let required_edges = vec![]; - let problem = RuralPostman::new(graph, edge_lengths, required_edges); + let problem = RuralPostman::new(graph, edge_lengths, required_edges).unwrap(); let config = vec![0, 0, 0]; assert_eq!(problem.evaluate(&config).unwrap(), Min(Some(0))); } @@ -102,10 +110,10 @@ fn test_rural_postman_no_edges_no_required() { #[test] fn test_rural_postman_disconnected_selection() { // Select two disconnected triangles — even degree but not connected - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3)]); + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3)]).unwrap(); let edge_lengths = vec![1, 1, 1, 1, 1, 1]; let required_edges = vec![0, 3]; // edges in different components - let problem = RuralPostman::new(graph, edge_lengths, required_edges); + let problem = RuralPostman::new(graph, edge_lengths, required_edges).unwrap(); // Select both triangles: even degree but disconnected let config = vec![1, 1, 1, 1, 1, 1]; assert_eq!(problem.evaluate(&config).unwrap(), Min(None)); @@ -170,7 +178,7 @@ fn test_rural_postman_problem_name() { #[test] fn test_rural_postman_set_weights() { let mut problem = chinese_postman_rpp(); - problem.set_weights(vec![2, 2, 2, 2]); + problem.set_weights(vec![2, 2, 2, 2]).unwrap(); assert_eq!(problem.weights(), vec![2, 2, 2, 2]); } 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 5135bd671..0de3f481e 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,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_nonpositive_edge_values() { @@ -8,7 +7,7 @@ fn create_spec_rejects_nonpositive_edge_values() { "edge_lengths" ); let result = ShortestWeightConstrainedPath::try_from(ShortestWeightConstrainedPathCreateSpec { - graph: SimpleGraph::new(2, vec![(0, 1)]), + graph: SimpleGraph::new(2, vec![(0, 1)]).unwrap(), edge_lengths: vec![0], edge_weights: vec![1], source_vertex: 0, @@ -36,13 +35,15 @@ fn issue_problem() -> ShortestWeightConstrainedPath { (4, 5), (1, 4), ], - ), + ) + .unwrap(), vec![2, 4, 3, 1, 5, 4, 2, 6], vec![5, 1, 2, 3, 2, 3, 1, 1], 0, 5, 8, ) + .unwrap() } #[test] @@ -53,7 +54,10 @@ fn test_shortest_weight_constrained_path_creation() { assert_eq!(problem.source_vertex(), 0); assert_eq!(problem.target_vertex(), 5); assert_eq!(*problem.weight_bound(), 8); - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); assert!(problem.is_weighted()); } @@ -108,8 +112,8 @@ fn test_shortest_weight_constrained_path_evaluation() { #[test] fn test_shortest_weight_constrained_path_accessors() { let mut problem = issue_problem(); - problem.set_lengths(vec![1, 1, 1, 1, 1, 1, 1, 1]); - problem.set_weights(vec![2, 2, 2, 2, 2, 2, 2, 2]); + problem.set_lengths(vec![1, 1, 1, 1, 1, 1, 1, 1]).unwrap(); + problem.set_weights(vec![2, 2, 2, 2, 2, 2, 2, 2]).unwrap(); assert_eq!(problem.edge_lengths(), &[1, 1, 1, 1, 1, 1, 1, 1]); assert_eq!(problem.edge_weights(), &[2, 2, 2, 2, 2, 2, 2, 2]); } @@ -147,13 +151,15 @@ fn test_shortest_weight_constrained_path_no_solution() { (4, 5), (1, 4), ], - ), + ) + .unwrap(), vec![2, 4, 3, 1, 5, 4, 2, 6], vec![5, 1, 2, 3, 2, 3, 1, 1], 0, 5, 3, - ); + ) + .unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); } @@ -215,13 +221,14 @@ fn test_shortest_weight_constrained_path_rejects_invalid_configs() { #[test] fn test_shortest_weight_constrained_path_source_equals_target_allows_only_empty_path() { let problem = ShortestWeightConstrainedPath::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![3, 4], vec![2, 5], 1, 1, 1, - ); + ) + .unwrap(); assert_eq!(problem.is_valid_solution(&[false, false]).unwrap(), Some(0)); assert_eq!(problem.is_valid_solution(&[true, false]).unwrap(), None); @@ -231,13 +238,14 @@ fn test_shortest_weight_constrained_path_source_equals_target_allows_only_empty_ fn test_shortest_weight_constrained_path_exceeds_weight_bound() { // Path 0-1 with weight 5 > weight_bound 3 let problem = ShortestWeightConstrainedPath::new( - SimpleGraph::new(2, vec![(0, 1)]), + SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![1], vec![5], 0, 1, 3, - ); + ) + .unwrap(); // Valid path but weight 5 > 3 assert_eq!(problem.is_valid_solution(&[true]).unwrap(), None); assert_eq!(problem.evaluate(&vec![true]).unwrap(), Min(None)); @@ -246,13 +254,14 @@ fn test_shortest_weight_constrained_path_exceeds_weight_bound() { #[test] fn test_shortest_weight_constrained_path_rejects_disconnected_selected_edges() { let problem = ShortestWeightConstrainedPath::new( - SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5), (5, 3)]), + SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5), (5, 3)]).unwrap(), vec![1, 1, 1, 1, 1], vec![1, 1, 1, 1, 1], 0, 2, 10, - ); + ) + .unwrap(); assert_eq!( problem @@ -263,27 +272,27 @@ fn test_shortest_weight_constrained_path_rejects_disconnected_selected_edges() { } #[test] -#[should_panic(expected = "All edge lengths must be positive (> 0)")] fn test_shortest_weight_constrained_path_rejects_non_positive_edge_lengths() { - ShortestWeightConstrainedPath::new( - SimpleGraph::new(2, vec![(0, 1)]), + assert!(ShortestWeightConstrainedPath::new( + SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![0], vec![1], 0, 1, 1, - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "weight_bound must be positive (> 0)")] fn test_shortest_weight_constrained_path_rejects_non_positive_bounds() { - ShortestWeightConstrainedPath::new( - SimpleGraph::new(2, vec![(0, 1)]), + assert!(ShortestWeightConstrainedPath::new( + SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![1], vec![1], 0, 1, 0, - ); + ) + .is_err()); } diff --git a/src/unit_tests/models/graph/spin_glass.rs b/src/unit_tests/models/graph/spin_glass.rs index 27cc19d84..a20d4cf07 100644 --- a/src/unit_tests/models/graph/spin_glass.rs +++ b/src/unit_tests/models/graph/spin_glass.rs @@ -13,9 +13,9 @@ fn create_spec_defaults_couplings_and_fields() { assert_eq!(problem.couplings(), &[1]); assert_eq!(problem.fields(), &[0, 0, 0]); } +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::traits::Problem; -include!("../../jl_helpers.rs"); #[test] fn test_spin_glass_creation() { @@ -94,12 +94,12 @@ fn test_compute_energy_rejects_invalid_spin_configuration() { #[test] fn test_num_variables() { let problem = SpinGlass::::without_fields(5, vec![]).unwrap(); - assert_eq!(problem.num_variables(), 5); + assert_eq!(problem.num_variables().unwrap(), 5); } #[test] fn test_from_graph() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = SpinGlass::::from_graph(graph, vec![1.0, 2.0], vec![0.0, 0.0, 0.0]) .unwrap(); @@ -110,7 +110,7 @@ fn test_from_graph() { #[test] fn test_from_graph_without_fields() { - let graph = SimpleGraph::new(2, vec![(0, 1)]); + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); let problem = SpinGlass::::from_graph_without_fields(graph, vec![1.5]).unwrap(); assert_eq!(problem.num_spins(), 2); diff --git a/src/unit_tests/models/graph/steiner_tree.rs b/src/unit_tests/models/graph/steiner_tree.rs index 92d4d7a29..8a3effacb 100644 --- a/src/unit_tests/models/graph/steiner_tree.rs +++ b/src/unit_tests/models/graph/steiner_tree.rs @@ -1,11 +1,10 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_duplicate_terminals() { assert_eq!(SteinerTreeCreateSpec::::FIELDS[2].name, "terminals"); let result = SteinerTree::try_from(SteinerTreeCreateSpec { - graph: SimpleGraph::new(2, vec![(0, 1)]), + graph: SimpleGraph::new(2, vec![(0, 1)]).unwrap(), edge_weights: vec![1], terminals: vec![0, 0], }); @@ -19,7 +18,8 @@ fn example_instance() -> SteinerTree { let graph = SimpleGraph::new( 5, vec![(0, 1), (0, 3), (1, 2), (1, 3), (2, 3), (2, 4), (3, 4)], - ); + ) + .unwrap(); let edge_weights = vec![2, 5, 2, 1, 5, 6, 1]; let terminals = vec![0, 2, 4]; SteinerTree::new(graph, edge_weights, terminals) @@ -31,13 +31,18 @@ fn test_steiner_tree_creation() { assert_eq!(problem.graph().num_vertices(), 5); assert_eq!(problem.graph().num_edges(), 7); assert_eq!(problem.terminals(), &[0, 2, 4]); - assert_eq!(problem.dimensions().len(), 7); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 7 + ); } #[test] #[should_panic(expected = "terminals must be distinct")] fn test_steiner_tree_rejects_duplicate_terminals() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let _ = SteinerTree::new(graph, vec![1, 1], vec![0, 0]); } @@ -96,7 +101,7 @@ fn test_steiner_tree_brute_force() { #[test] fn test_steiner_tree_all_terminals() { // When T = V, reduces to minimum spanning tree - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); let edge_weights = vec![1, 2, 3]; let terminals = vec![0, 1, 2]; let problem = SteinerTree::new(graph, edge_weights, terminals); @@ -117,7 +122,7 @@ fn test_steiner_tree_is_weighted() { // One has IS_UNIT = true, so is_weighted() returns false use crate::types::One; - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); let unweighted: SteinerTree = SteinerTree::unit_weights(graph, vec![0, 1, 2]); assert!(!unweighted.is_weighted()); } @@ -150,7 +155,7 @@ fn test_steiner_tree_disconnected_non_terminal_edges() { // Graph: path 0-1-2-3-4, terminals {0, 2} // Select edges (0,1), (1,2), (3,4) — terminals connected but vertex 3,4 form // a disconnected component of selected edges (not a tree). - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(); let edge_weights = vec![1, 1, 1, 1]; let terminals = vec![0, 2]; let problem = SteinerTree::new(graph, edge_weights, terminals); @@ -176,34 +181,34 @@ fn test_steiner_tree_edge_weights_and_set_weights() { } #[test] -#[should_panic(expected = "at least 2 terminals required")] -fn test_steiner_tree_rejects_single_terminal() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let _ = SteinerTree::new(graph, vec![1, 1], vec![0]); +#[should_panic(expected = "at least one terminal required")] +fn test_steiner_tree_rejects_empty_terminals() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let _ = SteinerTree::new(graph, vec![1, 1], vec![]); } #[test] #[should_panic(expected = "terminal 5 out of range")] fn test_steiner_tree_rejects_out_of_range_terminal() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let _ = SteinerTree::new(graph, vec![1, 1], vec![0, 5]); } #[test] #[should_panic(expected = "edge_weights length must match num_edges")] fn test_steiner_tree_rejects_wrong_weight_count() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let _ = SteinerTree::new(graph, vec![1, 1, 1], vec![0, 2]); } #[test] fn test_steiner_tree_deserialization_rejects_invalid_invariants() { - let one_terminal = serde_json::json!({ + let no_terminals = serde_json::json!({ "graph": {"num_vertices": 2, "edges": [[0, 1]]}, "edge_weights": [1], - "terminals": [0] + "terminals": [] }); - assert!(serde_json::from_value::>(one_terminal).is_err()); + assert!(serde_json::from_value::>(no_terminals).is_err()); let wrong_weights = serde_json::json!({ "graph": {"num_vertices": 2, "edges": [[0, 1]]}, @@ -212,3 +217,24 @@ fn test_steiner_tree_deserialization_rejects_invalid_invariants() { }); assert!(serde_json::from_value::>(wrong_weights).is_err()); } + +#[test] +fn test_steiner_tree_single_terminal_semantics() { + let problem = SteinerTree::try_from(SteinerTreeCreateSpec { + graph: SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2), (2, 3)]).unwrap(), + edge_weights: vec![-5, 1, 1, -10], + terminals: vec![0], + }) + .unwrap(); + let restored: SteinerTree = + serde_json::from_value(serde_json::to_value(&problem).unwrap()).unwrap(); + for (edges, value) in [ + (vec![false, false, false, false], Min(Some(0))), + (vec![true, false, false, false], Min(Some(-5))), + (vec![false, false, false, true], Min(None)), + (vec![true, false, false, true], Min(None)), + (vec![true, true, true, false], Min(None)), + ] { + assert_eq!(restored.evaluate(&edges).unwrap(), value); + } +} diff --git a/src/unit_tests/models/graph/steiner_tree_in_graphs.rs b/src/unit_tests/models/graph/steiner_tree_in_graphs.rs deleted file mode 100644 index fd845b214..000000000 --- a/src/unit_tests/models/graph/steiner_tree_in_graphs.rs +++ /dev/null @@ -1,232 +0,0 @@ -use super::*; -use crate::solvers::BruteForceProblem as _; - -#[test] -fn create_spec_defaults_edge_weights() { - let p = SteinerTreeInGraphs::try_from(SteinerTreeInGraphsCreateSpec:: { - graph: SimpleGraph::new(2, vec![(0, 1)]), - terminals: vec![0, 1], - edge_weights: None, - }) - .unwrap(); - assert_eq!(p.weights(), &[1]); -} -use crate::solvers::BruteForce; -use crate::topology::SimpleGraph; -use crate::traits::Problem; - -#[test] -fn test_steiner_tree_creation() { - // Path graph: 0-1-2-3 - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 3], vec![1i64, 2, 3]); - assert_eq!(problem.graph().num_vertices(), 4); - assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.terminals(), &[0, 3]); - assert_eq!(problem.dimensions().len(), 3); - assert_eq!(problem.num_vertices(), 4); - assert_eq!(problem.num_edges(), 3); - assert_eq!(problem.num_terminals(), 2); -} - -#[test] -fn test_steiner_tree_evaluation() { - // Triangle graph: 0-1, 1-2, 0-2, with terminal {0, 2} - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![3i64, 4, 1]); - - // Select edge 0-2 (weight 1): valid, connects terminals directly - let config_direct = vec![false, false, true]; - let result = problem.evaluate(&config_direct).unwrap(); - assert!(result.is_valid()); - assert_eq!(result.unwrap(), 1); - - // Select edges 0-1 and 1-2 (weights 3+4=7): valid, connects via vertex 1 - let config_via = vec![true, true, false]; - let result = problem.evaluate(&config_via).unwrap(); - assert!(result.is_valid()); - assert_eq!(result.unwrap(), 7); - - // Select only edge 0-1: invalid (terminal 2 not reached) - let config_invalid = vec![true, false, false]; - let result = problem.evaluate(&config_invalid).unwrap(); - assert!(!result.is_valid()); - - // Select no edges: invalid - let config_empty = vec![false, false, false]; - let result = problem.evaluate(&config_empty).unwrap(); - assert!(!result.is_valid()); -} - -#[test] -fn test_steiner_tree_solver() { - // Diamond graph: - // 1 - // / \ - // 0 3 - // \ / - // 2 - // Edges: 0-1(w=2), 0-2(w=1), 1-3(w=2), 2-3(w=1) - // Terminals: {0, 3} - // Optimal path: 0-2-3 with weight 1+1=2 - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 3], vec![2, 1, 2, 1]); - - let solver = BruteForce::new(); - let solution = solver.solve(&problem).unwrap().unwrap(); - let value = problem.evaluate(&solution).unwrap(); - assert!(value.is_valid()); - assert_eq!(value.unwrap(), 2); - // Should select edges 0-2 and 2-3 - assert_eq!(solution, vec![false, true, false, true]); -} - -#[test] -fn test_steiner_tree_with_steiner_vertices() { - // Star graph: center vertex 1 connected to 0, 2, 3 - // Edges: 0-1(w=1), 1-2(w=1), 1-3(w=1) - // Terminals: {0, 2, 3} - // Optimal: use vertex 1 as Steiner vertex, select all 3 edges, weight = 3 - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (1, 3)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2, 3], vec![1i64; 3]); - - let solver = BruteForce::new(); - let solution = solver.solve(&problem).unwrap().unwrap(); - let value = problem.evaluate(&solution).unwrap(); - assert!(value.is_valid()); - assert_eq!(value.unwrap(), 3); - assert_eq!(solution, vec![true, true, true]); -} - -#[test] -fn test_steiner_tree_is_valid_solution() { - // Path graph: 0-1-2 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![1i64; 2]); - - // Valid: both edges selected - assert!(problem.is_valid_solution(&[1, 1])); - // Invalid: only first edge - assert!(!problem.is_valid_solution(&[1, 0])); - // Invalid: only second edge - assert!(!problem.is_valid_solution(&[0, 1])); - // Invalid: no edges - assert!(!problem.is_valid_solution(&[0, 0])); -} - -#[test] -fn test_steiner_tree_parameter_getters() { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2, 4], vec![1i64; 4]); - assert_eq!(problem.num_vertices(), 5); - assert_eq!(problem.num_edges(), 4); - assert_eq!(problem.num_terminals(), 3); -} - -#[test] -fn test_steiner_tree_problem_name() { - assert_eq!( - as Problem>::NAME, - "SteinerTreeInGraphs" - ); -} - -#[test] -fn test_steiner_tree_serialization() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![1i64; 2]); - let json = serde_json::to_string(&problem).unwrap(); - let deserialized: SteinerTreeInGraphs = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.graph().num_vertices(), 3); - assert_eq!(deserialized.terminals(), &[0, 2]); - assert_eq!(deserialized.num_edges(), 2); -} - -#[test] -fn test_steiner_tree_single_terminal() { - // Single terminal: any config (including empty) is valid - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![1], vec![1i64; 2]); - - // No edges needed for a single terminal - let result = problem.evaluate(&vec![false, false]).unwrap(); - assert!(result.is_valid()); - assert_eq!(result.unwrap(), 0); -} - -#[test] -fn test_steiner_tree_all_vertices_terminal() { - // When all vertices are terminals, it degenerates to spanning tree - // Path: 0-1-2 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 1, 2], vec![1i64; 2]); - - let solver = BruteForce::new(); - let solution = solver.solve(&problem).unwrap().unwrap(); - let value = problem.evaluate(&solution).unwrap(); - assert!(value.is_valid()); - assert_eq!(value.unwrap(), 2); -} - -#[test] -fn test_steiner_tree_edges_accessor() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![5i64, 10]); - let edges = problem.edges(); - assert_eq!(edges.len(), 2); - assert_eq!(edges[0].2, 5); - assert_eq!(edges[1].2, 10); -} - -#[test] -fn test_steiner_tree_weights_management() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let mut problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![1i64; 2]); - assert!(problem.is_weighted()); - assert_eq!(problem.weights(), vec![1, 1]); - - problem.set_weights(vec![5, 10]); - assert_eq!(problem.weights(), vec![5, 10]); -} - -#[test] -fn test_steiner_tree_example_from_issue() { - // Example from issue #255: - // Graph with 8 vertices {0,1,2,3,4,5,6,7} and 12 edges - // Terminals R = {0, 3, 5, 7} - let graph = SimpleGraph::new( - 8, - vec![ - (0, 1), // w=2, idx=0 - (0, 2), // w=3, idx=1 - (1, 2), // w=1, idx=2 - (1, 3), // w=4, idx=3 - (2, 4), // w=2, idx=4 - (3, 4), // w=3, idx=5 - (3, 5), // w=5, idx=6 - (4, 5), // w=1, idx=7 - (4, 6), // w=2, idx=8 - (5, 6), // w=3, idx=9 - (5, 7), // w=4, idx=10 - (6, 7), // w=1, idx=11 - ], - ); - let weights = vec![2, 3, 1, 4, 2, 3, 5, 1, 2, 3, 4, 1]; - let problem = SteinerTreeInGraphs::new(graph, vec![0, 3, 5, 7], weights); - - // Brute-force verification: independently confirm optimal weight is 12 - let solver = BruteForce::new(); - let solution = solver.solve(&problem).unwrap().unwrap(); - let value = problem.evaluate(&solution).unwrap(); - assert!(value.is_valid()); - assert_eq!(value.unwrap(), 12); - - // Verify the claimed optimal solution from the issue: - // Edges: {0,1}(2) + {1,2}(1) + {2,4}(2) + {3,4}(3) + {4,5}(1) + {4,6}(2) + {6,7}(1) = 12 - let config = vec![ - true, false, true, false, true, true, false, true, true, false, false, true, - ]; - let result = problem.evaluate(&config).unwrap(); - assert!(result.is_valid()); - assert_eq!(result.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 107ca5e19..a0eada9d0 100644 --- a/src/unit_tests/models/graph/strong_connectivity_augmentation.rs +++ b/src/unit_tests/models/graph/strong_connectivity_augmentation.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -18,6 +17,7 @@ fn issue_graph() -> DirectedGraph { (5, 3), ], ) + .unwrap() } fn issue_candidate_arcs() -> Vec<(usize, usize, i64)> { @@ -56,7 +56,7 @@ fn yes_config() -> Vec { fn issue_example_already_strongly_connected() -> StrongConnectivityAugmentation { StrongConnectivityAugmentation::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(), vec![(0, 2, 5)], 0, ) @@ -71,7 +71,10 @@ fn test_strong_connectivity_augmentation_creation() { assert_eq!(problem.num_potential_arcs(), 18); assert_eq!(problem.candidate_arcs().len(), 18); assert_eq!(problem.bound(), &1); - assert_eq!(problem.dimensions(), vec![2; 18]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 18] + ); assert!(problem.is_weighted()); } @@ -103,7 +106,10 @@ fn test_strong_connectivity_augmentation_wrong_length() { #[test] fn test_strong_connectivity_augmentation_already_strongly_connected() { let problem = issue_example_already_strongly_connected(); - assert_eq!(problem.dimensions(), vec![2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2] + ); assert!(problem.evaluate(&vec![false]).unwrap()); assert!(!problem.evaluate(&vec![true]).unwrap()); } @@ -141,7 +147,7 @@ fn test_strong_connectivity_augmentation_variant() { #[should_panic(expected = "candidate arc (0, 1) already exists in the base graph")] fn test_strong_connectivity_augmentation_existing_arc_candidate_panics() { StrongConnectivityAugmentation::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![(0, 1, 1)], 1, ); @@ -151,7 +157,7 @@ fn test_strong_connectivity_augmentation_existing_arc_candidate_panics() { #[should_panic(expected = "duplicate candidate arc (0, 2)")] fn test_strong_connectivity_augmentation_duplicate_candidate_arc_panics() { StrongConnectivityAugmentation::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![(0, 2, 1), (0, 2, 3)], 3, ); @@ -161,7 +167,7 @@ fn test_strong_connectivity_augmentation_duplicate_candidate_arc_panics() { #[should_panic(expected = "candidate arc (0, 3) references vertex >= num_vertices")] fn test_strong_connectivity_augmentation_out_of_range_candidate_panics() { StrongConnectivityAugmentation::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![(0, 3, 1)], 1, ); diff --git a/src/unit_tests/models/graph/subgraph_isomorphism.rs b/src/unit_tests/models/graph/subgraph_isomorphism.rs index bde097812..315eb0789 100644 --- a/src/unit_tests/models/graph/subgraph_isomorphism.rs +++ b/src/unit_tests/models/graph/subgraph_isomorphism.rs @@ -1,28 +1,30 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; #[test] fn test_subgraph_isomorphism_creation() { - let host = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); - let pattern = SimpleGraph::new(2, vec![(0, 1)]); + let host = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); + let pattern = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); assert_eq!(problem.num_host_vertices(), 4); assert_eq!(problem.num_host_edges(), 3); assert_eq!(problem.num_pattern_vertices(), 2); assert_eq!(problem.num_pattern_edges(), 1); // dims: 2 pattern vertices, each can map to 4 host vertices - assert_eq!(problem.dimensions(), vec![4, 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 4] + ); } #[test] fn test_subgraph_isomorphism_evaluation_valid() { // Host: triangle 0-1-2 - let host = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let host = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); // Pattern: single edge - let pattern = SimpleGraph::new(2, vec![(0, 1)]); + let pattern = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); // Valid mapping: pattern vertex 0->host 0, pattern vertex 1->host 1 @@ -36,9 +38,9 @@ fn test_subgraph_isomorphism_evaluation_valid() { #[test] fn test_subgraph_isomorphism_evaluation_invalid() { // Host: path 0-1-2 (no edge 0-2) - let host = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let host = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); // Pattern: single edge - let pattern = SimpleGraph::new(2, vec![(0, 1)]); + let pattern = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); // Invalid: non-injective (both map to same host vertex) @@ -52,9 +54,9 @@ fn test_subgraph_isomorphism_evaluation_invalid() { #[test] fn test_subgraph_isomorphism_triangle_in_k4() { // Host: K4 (complete graph on 4 vertices) - let host = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); + let host = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); // Pattern: triangle K3 - let pattern = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); + let pattern = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); // Any injective mapping into K4 should work for K3 @@ -69,9 +71,9 @@ fn test_subgraph_isomorphism_triangle_in_k4() { #[test] fn test_subgraph_isomorphism_no_solution() { // Host: path 0-1-2 (no triangles) - let host = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let host = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); // Pattern: triangle K3 - let pattern = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); + let pattern = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); // No possible mapping should work @@ -83,9 +85,9 @@ fn test_subgraph_isomorphism_no_solution() { #[test] fn test_subgraph_isomorphism_solver() { // Host: K4 - let host = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); + let host = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); // Pattern: triangle - let pattern = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); + let pattern = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); let solver = BruteForce::new(); @@ -99,9 +101,9 @@ fn test_subgraph_isomorphism_solver() { #[test] fn test_subgraph_isomorphism_all_satisfying() { // Host: triangle 0-1-2 - let host = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let host = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); // Pattern: single edge - let pattern = SimpleGraph::new(2, vec![(0, 1)]); + let pattern = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); let solver = BruteForce::new(); @@ -115,8 +117,8 @@ fn test_subgraph_isomorphism_all_satisfying() { #[test] fn test_subgraph_isomorphism_serialization() { - let host = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let pattern = SimpleGraph::new(2, vec![(0, 1)]); + let host = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let pattern = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); let json = serde_json::to_string(&problem).unwrap(); @@ -138,8 +140,8 @@ fn test_subgraph_isomorphism_problem_name() { #[test] fn test_subgraph_isomorphism_is_valid_solution() { - let host = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let pattern = SimpleGraph::new(2, vec![(0, 1)]); + let host = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let pattern = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); assert!(problem.is_valid_solution(&[0, 1]).unwrap()); @@ -149,8 +151,8 @@ fn test_subgraph_isomorphism_is_valid_solution() { #[test] fn test_subgraph_isomorphism_empty_pattern() { // Pattern with no edges — any injective mapping is valid - let host = SimpleGraph::new(3, vec![(0, 1)]); - let pattern = SimpleGraph::new(2, vec![]); + let host = SimpleGraph::new(3, vec![(0, 1)]).unwrap(); + let pattern = SimpleGraph::new(2, vec![]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); // Any two distinct host vertices work @@ -179,9 +181,11 @@ fn test_subgraph_isomorphism_issue_example() { (4, 6), (5, 6), ], - ); + ) + .unwrap(); // Pattern: K4 - let pattern = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); + let pattern = + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); // The mapping from the issue: a->0, b->1, c->2, d->3 @@ -196,8 +200,8 @@ fn test_subgraph_isomorphism_issue_example() { #[test] fn test_subgraph_isomorphism_parameter_getters() { - let host = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); - let pattern = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let host = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(); + let pattern = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); assert_eq!(problem.num_host_vertices(), 5); assert_eq!(problem.num_host_edges(), 4); diff --git a/src/unit_tests/models/graph/traveling_salesman.rs b/src/unit_tests/models/graph/traveling_salesman.rs index ef89561a5..4783aa0d8 100644 --- a/src/unit_tests/models/graph/traveling_salesman.rs +++ b/src/unit_tests/models/graph/traveling_salesman.rs @@ -1,15 +1,15 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Min; fn k4_tsp() -> TravelingSalesman { TravelingSalesman::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), vec![10, 15, 20, 35, 25, 30], ) + .unwrap() } #[test] @@ -18,16 +18,20 @@ fn test_traveling_salesman_creation() { let problem = k4_tsp(); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 6); - assert_eq!(problem.dimensions().len(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 6 + ); } #[test] fn test_traveling_salesman_unit_weights() { // i64 type is always considered weighted, even with uniform values - let problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], - )); + let problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(), + ); assert!(problem.is_weighted()); assert_eq!(problem.graph().num_vertices(), 5); assert_eq!(problem.graph().num_edges(), 5); @@ -42,10 +46,9 @@ fn test_traveling_salesman_weighted() { #[test] fn test_evaluate_valid_cycle() { // C5 cycle graph with unit weights: all 5 edges form the only Hamiltonian cycle - let problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], - )); + let problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(), + ); // Select all edges -> valid Hamiltonian cycle, cost = 5 assert_eq!( problem @@ -72,10 +75,9 @@ fn test_evaluate_invalid_degree() { #[test] fn test_evaluate_invalid_not_connected() { // 6 vertices, two disjoint triangles: 0-1-2-0 and 3-4-5-3 - let problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 6, - vec![(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5)], - )); + let problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(6, vec![(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5)]).unwrap(), + ); // Select all 6 edges: two disjoint cycles, not a single Hamiltonian cycle assert_eq!( problem @@ -88,10 +90,9 @@ fn test_evaluate_invalid_not_connected() { #[test] fn test_evaluate_invalid_wrong_edge_count() { // C5 with only 4 edges selected -> not enough edges - let problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], - )); + let problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(), + ); assert_eq!( problem .evaluate(&vec![true, true, true, true, false]) @@ -102,10 +103,9 @@ fn test_evaluate_invalid_wrong_edge_count() { #[test] fn test_evaluate_no_edges_selected() { - let problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], - )); + let problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(), + ); assert_eq!( problem .evaluate(&vec![false, false, false, false, false]) @@ -130,10 +130,9 @@ fn test_brute_force_k4() { #[test] fn test_brute_force_path_graph_no_solution() { // Instance 2 from issue: path graph, no Hamiltonian cycle exists - let problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 4, - vec![(0, 1), (1, 2), (2, 3)], - )); + let problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), + ); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(solutions.is_empty()); @@ -142,10 +141,9 @@ fn test_brute_force_path_graph_no_solution() { #[test] fn test_brute_force_c5_unique_solution() { // Instance 3 from issue: C5 cycle graph, unique Hamiltonian cycle - let problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], - )); + let problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(), + ); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert_eq!(solutions.len(), 1); @@ -156,10 +154,9 @@ fn test_brute_force_c5_unique_solution() { #[test] fn test_brute_force_bipartite_no_solution() { // Instance 4 from issue: K_{2,3} bipartite, no Hamiltonian cycle - let problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 5, - vec![(0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4)], - )); + let problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(5, vec![(0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4)]).unwrap(), + ); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(solutions.is_empty()); @@ -177,32 +174,32 @@ fn test_problem_name() { fn test_is_hamiltonian_cycle_function() { // Triangle: selecting all 3 edges is a valid Hamiltonian cycle assert!(is_hamiltonian_cycle( - &SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), &[true, true, true] )); // Path: not a cycle assert!(!is_hamiltonian_cycle( - &SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + &SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), &[true, true] )); } #[test] fn test_set_weights() { - let mut problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 3, - vec![(0, 1), (1, 2), (0, 2)], - )); - problem.set_weights(vec![5, 10, 15]); + let mut problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), + ); + problem.set_weights(vec![5, 10, 15]).unwrap(); assert_eq!(problem.weights(), vec![5, 10, 15]); } #[test] fn test_edges() { let problem = TravelingSalesman::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![10, 20, 30], - ); + ) + .unwrap(); let edges = problem.edges(); assert_eq!(edges.len(), 3); } @@ -210,19 +207,19 @@ fn test_edges() { #[test] fn test_new() { let problem = TravelingSalesman::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![10, 20, 30], - ); + ) + .unwrap(); assert_eq!(problem.graph().num_vertices(), 3); assert_eq!(problem.weights(), vec![10, 20, 30]); } #[test] fn test_unit_weights() { - let problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 3, - vec![(0, 1), (1, 2), (0, 2)], - )); + let problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), + ); assert_eq!(problem.weights(), vec![1, 1, 1]); } @@ -230,9 +227,10 @@ fn test_unit_weights() { fn test_brute_force_triangle_weighted() { // Triangle with weights: unique Hamiltonian cycle using all edges let problem = TravelingSalesman::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![5, 10, 15], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert_eq!(solutions.len(), 1); @@ -244,9 +242,10 @@ fn test_brute_force_triangle_weighted() { fn test_is_valid_solution() { // K3 triangle: edges (0,1), (0,2), (1,2) — config is per edge let problem = TravelingSalesman::new( - SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(), vec![1, 2, 3], - ); + ) + .unwrap(); // Valid: select all 3 edges forms Hamiltonian cycle 0-1-2-0 assert!(problem.is_valid_solution(&[true, true, true])); // Invalid: select only 2 edges — not a cycle @@ -256,9 +255,10 @@ fn test_is_valid_solution() { #[test] fn test_parameter_getters() { let problem = TravelingSalesman::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_edges(), 3); } @@ -268,9 +268,10 @@ fn test_tsp_paper_example() { // Paper: K4, weights w(0,1)=1, w(0,2)=3, w(0,3)=2, w(1,2)=2, w(1,3)=3, w(2,3)=1 // Optimal tour: v0→v1→v2→v3→v0, cost = 1+2+1+2 = 6 let problem = TravelingSalesman::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), vec![1, 3, 2, 2, 3, 1], - ); + ) + .unwrap(); // Edges: 0=(0,1), 1=(0,2), 2=(0,3), 3=(1,2), 4=(1,3), 5=(2,3) // Tour uses edges 0, 2, 3, 5 let config = vec![true, false, true, true, false, true]; @@ -292,3 +293,21 @@ fn create_spec_uses_edge_weights_and_defaults_to_one() { assert_eq!(problem.weights(), vec![1, 1, 1]); assert_eq!(TravelingSalesmanCreateSpec::FIELDS[2].name, "edge_weights"); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); + assert!(TravelingSalesman::new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "edge_weights": []}); + assert!(serde_json::from_value::>(json).is_err()); +} + +#[test] +fn rejected_weight_update_preserves_instance() { + let mut problem = + TravelingSalesman::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![3i64; 1]).unwrap(); + let before = serde_json::to_value(&problem).unwrap(); + assert!(problem.set_weights(vec![]).is_err()); + assert_eq!(serde_json::to_value(&problem).unwrap(), before); + problem.set_weights(vec![4; 1]).unwrap(); +} 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 c67868b1c..3e60530c6 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,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_lower_bound_above_capacity() { @@ -9,7 +8,7 @@ fn create_spec_rejects_lower_bound_above_capacity() { ); assert!( UndirectedFlowLowerBounds::try_from(UndirectedFlowLowerBoundsCreateSpec { - graph: SimpleGraph::new(2, vec![(0, 1)]), + graph: SimpleGraph::new(2, vec![(0, 1)]).unwrap(), capacities: vec![1], lower_bounds: vec![2], source: 0, @@ -28,24 +27,27 @@ fn canonical_yes_instance() -> UndirectedFlowLowerBounds { SimpleGraph::new( 6, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 4), (3, 5), (4, 5)], - ), + ) + .unwrap(), vec![2, 2, 2, 2, 1, 3, 2], vec![1, 1, 0, 0, 1, 0, 1], 0, 5, 3, ) + .unwrap() } fn canonical_no_instance() -> UndirectedFlowLowerBounds { UndirectedFlowLowerBounds::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]).unwrap(), vec![2, 2, 1, 1], vec![2, 2, 1, 1], 0, 3, 2, ) + .unwrap() } fn yes_orientation_config() -> Vec { @@ -64,7 +66,10 @@ fn test_undirected_flow_lower_bounds_creation() { assert_eq!(problem.requirement(), 3); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 7); - assert_eq!(problem.dimensions(), vec![2; 7]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 7] + ); } #[test] 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 d09689d25..3d8993a59 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,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_validates_capacity_shape() { @@ -25,7 +24,7 @@ use crate::traits::Problem; fn canonical_instance() -> UndirectedTwoCommodityIntegralFlow { UndirectedTwoCommodityIntegralFlow::new( - SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]).unwrap(), vec![1, 1, 2], 0, 3, @@ -34,11 +33,12 @@ fn canonical_instance() -> UndirectedTwoCommodityIntegralFlow { 1, 1, ) + .unwrap() } fn shared_bottleneck_instance() -> UndirectedTwoCommodityIntegralFlow { UndirectedTwoCommodityIntegralFlow::new( - SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]).unwrap(), vec![1, 1, 1], 0, 3, @@ -47,6 +47,7 @@ fn shared_bottleneck_instance() -> UndirectedTwoCommodityIntegralFlow { 1, 1, ) + .unwrap() } fn example_config() -> Vec { @@ -72,7 +73,7 @@ fn test_undirected_two_commodity_integral_flow_creation() { assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); assert_eq!( - problem.dimensions(), + crate::solvers::cartesian_dimensions(&problem).unwrap(), vec![2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3] ); } @@ -155,7 +156,7 @@ fn test_undirected_two_commodity_integral_flow_large_capacity_sink_balance() { let large: i64 = 1_000_000; let large_usize = large as usize; let problem = UndirectedTwoCommodityIntegralFlow::new( - SimpleGraph::new(2, vec![(0, 1)]), + SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![large], 0, 1, @@ -163,7 +164,8 @@ fn test_undirected_two_commodity_integral_flow_large_capacity_sink_balance() { 1, large, 0, - ); + ) + .unwrap(); assert!(problem.evaluate(&vec![large_usize, 0, 0, 0]).unwrap()); } @@ -172,7 +174,7 @@ fn test_undirected_two_commodity_integral_flow_large_capacity_sink_balance() { fn test_undirected_two_commodity_integral_flow_shared_capacity_exceeded() { // Two commodities each sending 2 units on an edge with capacity 3. let problem = UndirectedTwoCommodityIntegralFlow::new( - SimpleGraph::new(2, vec![(0, 1)]), + SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![3], 0, 1, @@ -180,17 +182,17 @@ fn test_undirected_two_commodity_integral_flow_shared_capacity_exceeded() { 1, 2, 2, - ); + ) + .unwrap(); // f1(0->1)=2, f1(1->0)=0, f2(0->1)=2, f2(1->0)=0 => shared = 4 > 3 assert!(!problem.evaluate(&vec![2, 0, 2, 0]).unwrap()); } #[test] -#[should_panic(expected = "capacities length must match")] -fn test_undirected_two_commodity_integral_flow_panics_wrong_capacity_count() { - UndirectedTwoCommodityIntegralFlow::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), +fn test_undirected_two_commodity_integral_flow_rejects_wrong_capacity_count() { + assert!(UndirectedTwoCommodityIntegralFlow::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1], // 1 capacity but 2 edges 0, 2, @@ -198,14 +200,14 @@ fn test_undirected_two_commodity_integral_flow_panics_wrong_capacity_count() { 2, 1, 1, - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "must be less than num_vertices")] -fn test_undirected_two_commodity_integral_flow_panics_vertex_out_of_bounds() { - UndirectedTwoCommodityIntegralFlow::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), +fn test_undirected_two_commodity_integral_flow_rejects_vertex_out_of_bounds() { + assert!(UndirectedTwoCommodityIntegralFlow::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1, 1], 0, 5, // out of bounds @@ -213,14 +215,15 @@ fn test_undirected_two_commodity_integral_flow_panics_vertex_out_of_bounds() { 2, 1, 1, - ); + ) + .is_err()); } #[test] fn test_undirected_two_commodity_integral_flow_flow_conservation_violated() { // 0 -- 1 -- 2, commodity 1: s=0 t=2, commodity 2: s=0 t=2 let problem = UndirectedTwoCommodityIntegralFlow::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![2, 2], 0, 2, @@ -228,7 +231,8 @@ fn test_undirected_two_commodity_integral_flow_flow_conservation_violated() { 2, 1, 1, - ); + ) + .unwrap(); // Flow conservation violated at vertex 1: commodity 1 enters but doesn't leave. // Edge (0,1): f1(0->1)=1, f1(1->0)=0, f2=0,0 diff --git a/src/unit_tests/models/misc/additional_key.rs b/src/unit_tests/models/misc/additional_key.rs index 8df337615..68186b42a 100644 --- a/src/unit_tests/models/misc/additional_key.rs +++ b/src/unit_tests/models/misc/additional_key.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; /// Instance 1: 6 attributes, cyclic FDs, 3 known keys. @@ -17,11 +16,12 @@ fn instance1() -> AdditionalKey { vec![0, 1, 2, 3, 4, 5], vec![vec![0, 1], vec![2, 3], vec![4, 5]], ) + .unwrap() } /// Instance 2: 3 attributes, single FD {0}->{1,2}, known key [{0}]. fn instance2() -> AdditionalKey { - AdditionalKey::new(3, vec![(vec![0], vec![1, 2])], vec![0, 1, 2], vec![vec![0]]) + AdditionalKey::new(3, vec![(vec![0], vec![1, 2])], vec![0, 1, 2], vec![vec![0]]).unwrap() } #[test] @@ -31,7 +31,10 @@ fn test_additional_key_creation() { assert_eq!(problem.num_dependencies(), 5); assert_eq!(problem.num_relation_attrs(), 6); assert_eq!(problem.num_known_keys(), 3); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2, 2, 2] + ); assert_eq!(::NAME, "AdditionalKey"); assert_eq!(::variant(), vec![]); // Data getters @@ -170,31 +173,26 @@ fn test_additional_key_empty_selection() { } #[test] -#[should_panic(expected = "relation_attrs element")] -fn test_additional_key_panic_relation_attrs_out_of_bounds() { - AdditionalKey::new(3, vec![], vec![0, 1, 5], vec![]); +fn test_additional_key_rejects_relation_attrs_out_of_bounds() { + assert!(AdditionalKey::new(3, vec![], vec![0, 1, 5], vec![]).is_err()); } #[test] -#[should_panic(expected = "relation_attrs contains duplicates")] -fn test_additional_key_panic_relation_attrs_duplicates() { - AdditionalKey::new(3, vec![], vec![0, 1, 1], vec![]); +fn test_additional_key_rejects_relation_attrs_duplicates() { + assert!(AdditionalKey::new(3, vec![], vec![0, 1, 1], vec![]).is_err()); } #[test] -#[should_panic(expected = "dependency lhs attribute")] -fn test_additional_key_panic_dependency_lhs_out_of_bounds() { - AdditionalKey::new(3, vec![(vec![5], vec![0])], vec![0, 1, 2], vec![]); +fn test_additional_key_rejects_dependency_lhs_out_of_bounds() { + assert!(AdditionalKey::new(3, vec![(vec![5], vec![0])], vec![0, 1, 2], vec![]).is_err()); } #[test] -#[should_panic(expected = "dependency rhs attribute")] -fn test_additional_key_panic_dependency_rhs_out_of_bounds() { - AdditionalKey::new(3, vec![(vec![0], vec![5])], vec![0, 1, 2], vec![]); +fn test_additional_key_rejects_dependency_rhs_out_of_bounds() { + assert!(AdditionalKey::new(3, vec![(vec![0], vec![5])], vec![0, 1, 2], vec![]).is_err()); } #[test] -#[should_panic(expected = "known_keys attribute")] -fn test_additional_key_panic_known_keys_out_of_bounds() { - AdditionalKey::new(3, vec![], vec![0, 1, 2], vec![vec![5]]); +fn test_additional_key_rejects_known_keys_out_of_bounds() { + assert!(AdditionalKey::new(3, vec![], vec![0, 1, 2], vec![vec![5]]).is_err()); } diff --git a/src/unit_tests/models/misc/betweenness.rs b/src/unit_tests/models/misc/betweenness.rs index ffd895343..cff1d9ba8 100644 --- a/src/unit_tests/models/misc/betweenness.rs +++ b/src/unit_tests/models/misc/betweenness.rs @@ -17,8 +17,11 @@ fn test_betweenness_basic() { problem.triples(), &[(0, 1, 2), (2, 3, 4), (0, 2, 4), (1, 3, 4)] ); - assert_eq!(problem.dimensions(), vec![5; 5]); - assert_eq!(problem.num_variables(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 5] + ); + assert_eq!(problem.num_variables().unwrap(), 5); assert_eq!(::NAME, "Betweenness"); assert_eq!(::variant(), vec![]); } diff --git a/src/unit_tests/models/misc/bin_packing.rs b/src/unit_tests/models/misc/bin_packing.rs index b0a8e77eb..02d10d8c4 100644 --- a/src/unit_tests/models/misc/bin_packing.rs +++ b/src/unit_tests/models/misc/bin_packing.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -9,9 +8,17 @@ fn test_bin_packing_creation() { assert_eq!(problem.num_items(), 6); assert_eq!(problem.sizes(), &[6, 6, 5, 5, 4, 4]); assert_eq!(*problem.capacity(), 10); - assert_eq!(problem.dimensions().len(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 6 + ); // Each variable has domain {0, ..., 5} - assert!(problem.dimensions().iter().all(|&d| d == 6)); + assert!(crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .iter() + .all(|&d| d == 6)); } #[test] @@ -92,7 +99,10 @@ fn test_bin_packing_brute_force_small() { fn test_bin_packing_empty_items() { let problem = BinPacking::new(Vec::::new(), 10).unwrap(); assert_eq!(problem.num_items(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); let result = problem.evaluate(&vec![]).unwrap(); assert!(result.is_valid()); assert_eq!(result.unwrap(), 0); 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 a15ea8bd2..c975caa10 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 @@ -14,6 +14,7 @@ fn canonical_problem() -> BoyceCoddNormalFormViolation { ], vec![0, 1, 2, 3, 4, 5], ) + .unwrap() } #[test] @@ -38,8 +39,11 @@ fn test_bcnf_creation() { assert_eq!(problem.num_attributes(), 6); assert_eq!(problem.num_functional_deps(), 3); assert_eq!(problem.num_target_attributes(), 6); - assert_eq!(problem.num_variables(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!(problem.num_variables().unwrap(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!(problem.target_subset(), &[0, 1, 2, 3, 4, 5]); assert_eq!(problem.functional_deps().len(), 3); } @@ -112,7 +116,8 @@ fn test_bcnf_solver_finds_violation() { #[test] 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 problem = + BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![0])], vec![0, 1, 2]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(solutions.is_empty()); @@ -123,7 +128,8 @@ fn test_bcnf_partial_target_subset() { // Only test a subset of attributes. // FD: {0} → {1}; target = {0, 1}. // X = {0}: closure = {0, 1}. A' \ X = {1}. 1 ∈ closure but nothing is outside → no violation. - let problem = BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![1])], vec![0, 1]); + let problem = + BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![1])], vec![0, 1]).unwrap(); assert!(!problem.evaluate(&vec![true, false]).unwrap()); // X={0}: all of A'\X = {1} ⊆ closure → no violation assert!(!problem.evaluate(&vec![false, false]).unwrap()); // X={}: closure={}, nothing in closure → no violation } @@ -132,7 +138,8 @@ fn test_bcnf_partial_target_subset() { fn test_bcnf_violation_with_three_attrs_in_target() { // Attrs 0,1,2. FD: {0} → {1}. Target = {0, 1, 2}. // X = {0}: closure = {0, 1}. A' \ X = {1, 2}. 1 ∈ closure, 2 ∉ closure → BCNF violation. - let problem = BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![1])], vec![0, 1, 2]); + let problem = + BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![1])], vec![0, 1, 2]).unwrap(); assert!(problem.evaluate(&vec![true, false, false]).unwrap()); // X = {0} assert!(!problem.evaluate(&vec![false, true, false]).unwrap()); // X = {1}: A'\X = {0,2}, closure of {1} = {1}, 0∉closure, 2∉closure → no violation } @@ -153,41 +160,39 @@ fn test_bcnf_serialization() { } #[test] -#[should_panic(expected = "target_subset must be non-empty")] fn test_bcnf_rejects_empty_target_subset() { - BoyceCoddNormalFormViolation::new(3, vec![], vec![]); + assert!(BoyceCoddNormalFormViolation::new(3, vec![], vec![]).is_err()); } #[test] -#[should_panic(expected = "empty LHS")] fn test_bcnf_rejects_empty_lhs_fd() { - BoyceCoddNormalFormViolation::new(3, vec![(vec![], vec![1])], vec![0, 1]); + assert!(BoyceCoddNormalFormViolation::new(3, vec![(vec![], vec![1])], vec![0, 1]).is_err()); } #[test] -#[should_panic(expected = "out of range")] fn test_bcnf_rejects_out_of_range_fd_attr() { - BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![5])], vec![0, 1]); + assert!(BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![5])], vec![0, 1]).is_err()); } #[test] -#[should_panic(expected = "out of range")] fn test_bcnf_rejects_out_of_range_target_attr() { - BoyceCoddNormalFormViolation::new(3, vec![], vec![0, 5]); + assert!(BoyceCoddNormalFormViolation::new(3, vec![], vec![0, 5]).is_err()); } #[test] fn test_bcnf_deduplicates_fd_attrs() { // LHS with duplicates should be deduped without panic. let problem = - BoyceCoddNormalFormViolation::new(3, vec![(vec![0, 0], vec![1, 1])], vec![0, 1, 2]); + BoyceCoddNormalFormViolation::new(3, vec![(vec![0, 0], vec![1, 1])], vec![0, 1, 2]) + .unwrap(); assert_eq!(problem.functional_deps()[0].0, vec![0]); assert_eq!(problem.functional_deps()[0].1, vec![1]); } #[test] fn test_bcnf_deduplicates_target_subset() { - let problem = BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![1])], vec![0, 1, 0, 2]); + let problem = + BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![1])], vec![0, 1, 0, 2]).unwrap(); assert_eq!(problem.target_subset(), &[0, 1, 2]); assert_eq!(problem.num_target_attributes(), 3); } @@ -200,7 +205,8 @@ fn test_bcnf_fds_outside_target_subset() { 5, vec![(vec![0], vec![3]), (vec![3], vec![4])], vec![0, 1, 2], - ); + ) + .unwrap(); assert!(!problem.evaluate(&vec![true, false, false]).unwrap()); // X={0}: closure reaches {0,3,4} but A'\X={1,2} untouched } @@ -218,7 +224,8 @@ fn test_bcnf_cyclic_keys_no_violation() { (vec![1, 3], vec![0, 2]), ], vec![0, 1, 2, 3], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!( diff --git a/src/unit_tests/models/misc/capacity_assignment.rs b/src/unit_tests/models/misc/capacity_assignment.rs index 09894567c..393f9337e 100644 --- a/src/unit_tests/models/misc/capacity_assignment.rs +++ b/src/unit_tests/models/misc/capacity_assignment.rs @@ -1,6 +1,5 @@ use super::CapacityAssignmentCreateSpec; use crate::models::misc::CapacityAssignment; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_validates_monotonicity() { @@ -23,6 +22,7 @@ fn example_problem() -> CapacityAssignment { vec![vec![8, 4, 1], vec![7, 3, 1], vec![6, 3, 1]], 12, ) + .unwrap() } #[test] @@ -32,7 +32,10 @@ fn test_capacity_assignment_basic_properties() { assert_eq!(problem.num_capacities(), 3); assert_eq!(problem.capacities(), &[1, 2, 3]); assert_eq!(problem.delay_budget(), 12); - assert_eq!(problem.dimensions(), vec![3, 3, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3, 3] + ); assert_eq!(::NAME, "CapacityAssignment"); assert_eq!(::variant(), Vec::new()); } @@ -104,7 +107,7 @@ fn test_capacity_assignment_paper_example() { #[test] fn test_capacity_assignment_rejects_non_increasing_capacities() { let result = std::panic::catch_unwind(|| { - CapacityAssignment::new(vec![1, 1], vec![vec![1, 2]], vec![vec![2, 1]], 3) + CapacityAssignment::new(vec![1, 1], vec![vec![1, 2]], vec![vec![2, 1]], 3).unwrap() }); assert!(result.is_err()); } @@ -112,7 +115,7 @@ fn test_capacity_assignment_rejects_non_increasing_capacities() { #[test] fn test_capacity_assignment_rejects_non_monotone_delay_row() { let result = std::panic::catch_unwind(|| { - CapacityAssignment::new(vec![1, 2], vec![vec![1, 2]], vec![vec![1, 2]], 3) + CapacityAssignment::new(vec![1, 2], vec![vec![1, 2]], vec![vec![1, 2]], 3).unwrap() }); assert!(result.is_err()); } diff --git a/src/unit_tests/models/misc/closest_string.rs b/src/unit_tests/models/misc/closest_string.rs index fba543793..689341ae8 100644 --- a/src/unit_tests/models/misc/closest_string.rs +++ b/src/unit_tests/models/misc/closest_string.rs @@ -9,6 +9,7 @@ fn issue_instance() -> ClosestString { 2, vec![vec![0, 0, 0], vec![0, 1, 1], vec![1, 0, 1], vec![1, 1, 0]], ) + .unwrap() } #[test] @@ -18,8 +19,11 @@ fn test_closest_string_creation() { assert_eq!(problem.num_strings(), 4); assert_eq!(problem.string_length(), 3); assert_eq!(problem.total_length(), 12); - assert_eq!(problem.dimensions(), vec![2, 2, 2]); - assert_eq!(problem.num_variables(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2] + ); + assert_eq!(problem.num_variables().unwrap(), 3); assert_eq!(::NAME, "ClosestString"); assert_eq!(::variant(), vec![]); } @@ -77,21 +81,18 @@ fn test_closest_string_bruteforce_finds_optimum() { } #[test] -#[should_panic(expected = "ClosestString requires at least one input string")] fn test_closest_string_panics_on_empty_input_list() { - let _ = ClosestString::new(2, Vec::new()); + assert!(ClosestString::new(2, Vec::new()).is_err()); } #[test] -#[should_panic(expected = "all input strings must have the same length")] fn test_closest_string_panics_on_length_mismatch() { - let _ = ClosestString::new(2, vec![vec![0, 1, 0], vec![1, 0]]); + assert!(ClosestString::new(2, vec![vec![0, 1, 0], vec![1, 0]]).is_err()); } #[test] -#[should_panic(expected = "input symbols must be less than alphabet_size")] fn test_closest_string_panics_on_out_of_alphabet_symbol() { - let _ = ClosestString::new(2, vec![vec![0, 1, 2]]); + assert!(ClosestString::new(2, vec![vec![0, 1, 2]]).is_err()); } #[test] @@ -100,8 +101,11 @@ fn test_closest_string_larger_alphabet_smoke() { // Inputs (01, 12, 20) are pairwise at Hamming distance 2, so any center // must have radius at least 2; e.g., c = 00 achieves d(00,01)=1, // d(00,12)=2, d(00,20)=1, giving a max of 2. - let problem = ClosestString::new(3, vec![vec![0, 1], vec![1, 2], vec![2, 0]]); - assert_eq!(problem.dimensions(), vec![3, 3]); + let problem = ClosestString::new(3, vec![vec![0, 1], vec![1, 2], vec![2, 0]]).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3] + ); assert_eq!(problem.num_strings(), 3); assert_eq!(problem.string_length(), 2); let solver = BruteForce::new(); @@ -120,7 +124,10 @@ fn test_closest_string_serialization() { let restored: ClosestString = serde_json::from_value(json).unwrap(); assert_eq!(restored.alphabet_size(), problem.alphabet_size()); assert_eq!(restored.strings(), problem.strings()); - assert_eq!(restored.dimensions(), problem.dimensions()); + assert_eq!( + crate::solvers::cartesian_dimensions(&restored).unwrap(), + crate::solvers::cartesian_dimensions(&problem).unwrap() + ); assert_eq!( restored.evaluate(&vec![0, 0, 0]).unwrap(), problem.evaluate(&vec![0, 0, 0]).unwrap() diff --git a/src/unit_tests/models/misc/closest_substring.rs b/src/unit_tests/models/misc/closest_substring.rs index 1a98cfef5..47ff0ee22 100644 --- a/src/unit_tests/models/misc/closest_substring.rs +++ b/src/unit_tests/models/misc/closest_substring.rs @@ -4,8 +4,8 @@ use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; -fn issue_instance() -> ClosestSubstring { - // The #1033 canonical example: q = 2, ell = 3, three length-5 binary strings. +fn canonical_instance() -> ClosestSubstring { + // Canonical example: q = 2, ell = 3, three length-5 binary strings. ClosestSubstring::new( 2, vec![ @@ -20,24 +20,26 @@ fn issue_instance() -> ClosestSubstring { #[test] fn test_closest_substring_creation() { - let problem = issue_instance(); + let problem = canonical_instance(); assert_eq!(problem.alphabet_size(), 2); assert_eq!(problem.num_strings(), 3); assert_eq!(problem.substring_length(), 3); assert_eq!(problem.total_length(), 15); assert_eq!(problem.total_num_windows(), 9); - assert_eq!(problem.num_window_choice_product(), 27); // dims: 3 center slots (each of size 2) + one window-position slot per // string (each of size W_i = 5 - 3 + 1 = 3). - assert_eq!(problem.dimensions(), vec![2, 2, 2, 3, 3, 3]); - assert_eq!(problem.num_variables(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 3, 3, 3] + ); + assert_eq!(problem.num_variables().unwrap(), 6); assert_eq!(::NAME, "ClosestSubstring"); assert_eq!(::variant(), vec![]); } #[test] fn test_closest_substring_evaluate_at_optimum() { - let problem = issue_instance(); + let problem = canonical_instance(); // Center [0,1,0] with window picks (0, 1, 0): // s_1[0..3] = [0,0,0], d_H([0,1,0], [0,0,0]) = 1 // s_2[1..4] = [0,1,0], d_H = 0 @@ -51,7 +53,7 @@ fn test_closest_substring_evaluate_at_optimum() { #[test] fn test_closest_substring_evaluate_all_zero_windows() { - let problem = issue_instance(); + let problem = canonical_instance(); // c = [0,0,0], windows (0, 0, 0): // s_1[0..3] = [0,0,0] d = 0 // s_2[0..3] = [1,0,1] d = 2 @@ -65,7 +67,7 @@ fn test_closest_substring_evaluate_all_zero_windows() { #[test] fn test_closest_substring_evaluate_at_111_center() { - let problem = issue_instance(); + let problem = canonical_instance(); // Any center [1,1,1] has Hamming distance >= 1 to every length-3 binary // string that contains at least one 0. All windows of s_1, s_2, s_3 // contain at least one zero, so the radius is at least 1. @@ -79,7 +81,7 @@ fn test_closest_substring_evaluate_at_111_center() { #[test] fn test_closest_substring_evaluate_invalid_length() { - let problem = issue_instance(); + let problem = canonical_instance(); assert!(matches!( problem.evaluate(&vec![0, 0, 0]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -92,7 +94,7 @@ fn test_closest_substring_evaluate_invalid_length() { #[test] fn test_closest_substring_bruteforce_finds_optimum() { - let problem = issue_instance(); + let problem = canonical_instance(); let solver = BruteForce::new(); // 8 centers * 27 window combinations = 216 configurations; optimum is 1. assert_eq!( @@ -112,7 +114,7 @@ fn test_closest_substring_bruteforce_finds_optimum() { fn test_closest_substring_specializes_to_closest_string() { // When substring_length == string_length, each input string has exactly // one window (W_i = 1) and the problem reduces to ClosestString on the - // same instance. Use the #1032 canonical (4 binary strings of length 3), + // same instance. Use four binary strings of length 3, // whose optimum radius is 2. let problem = ClosestSubstring::new( 2, @@ -120,8 +122,10 @@ fn test_closest_substring_specializes_to_closest_string() { 3, ) .unwrap(); - assert_eq!(problem.num_window_choice_product(), 1); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 1, 1, 1, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 1, 1, 1, 1] + ); let solver = BruteForce::new(); assert_eq!( problem @@ -161,13 +165,16 @@ fn test_closest_substring_rejects_out_of_alphabet_symbol() { #[test] fn test_closest_substring_serialization() { - let problem = issue_instance(); + let problem = canonical_instance(); let json = serde_json::to_value(&problem).unwrap(); let restored: ClosestSubstring = serde_json::from_value(json).unwrap(); assert_eq!(restored.alphabet_size(), problem.alphabet_size()); assert_eq!(restored.strings(), problem.strings()); assert_eq!(restored.substring_length(), problem.substring_length()); - assert_eq!(restored.dimensions(), problem.dimensions()); + assert_eq!( + crate::solvers::cartesian_dimensions(&restored).unwrap(), + crate::solvers::cartesian_dimensions(&problem).unwrap() + ); assert_eq!( restored.evaluate(&vec![0, 1, 0, 0, 1, 0]).unwrap(), problem.evaluate(&vec![0, 1, 0, 0, 1, 0]).unwrap() diff --git a/src/unit_tests/models/misc/clustering.rs b/src/unit_tests/models/misc/clustering.rs index 6fcaccfec..c9e7c6a57 100644 --- a/src/unit_tests/models/misc/clustering.rs +++ b/src/unit_tests/models/misc/clustering.rs @@ -1,6 +1,5 @@ use crate::models::misc::Clustering; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; /// Helper: build the 6-element two-group instance from the issue. @@ -13,7 +12,7 @@ fn two_group_instance() -> Clustering { vec![3, 3, 3, 1, 0, 1], vec![3, 3, 3, 1, 1, 0], ]; - Clustering::new(distances, 2, 1) + Clustering::new(distances, 2, 1).unwrap() } #[test] @@ -23,7 +22,10 @@ fn test_clustering_creation() { assert_eq!(problem.num_clusters(), 2); assert_eq!(problem.diameter_bound(), 1); assert_eq!(problem.distances().len(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); } #[test] @@ -79,7 +81,7 @@ fn test_clustering_evaluate_invalid_cluster_index() { fn test_clustering_trivial_k_ge_n() { // K ≥ n: each element in its own cluster → always feasible let distances = vec![vec![0, 100, 100], vec![100, 0, 100], vec![100, 100, 0]]; - let problem = Clustering::new(distances, 3, 0); + let problem = Clustering::new(distances, 3, 0).unwrap(); // Each element in its own cluster: [0, 1, 2] assert!(problem.evaluate(&vec![0, 1, 2]).unwrap().0); } @@ -102,7 +104,7 @@ fn test_clustering_solver_all_witnesses() { vec![3, 3, 0, 1], vec![3, 3, 1, 0], ]; - let problem = Clustering::new(distances, 2, 1); + let problem = Clustering::new(distances, 2, 1).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -134,23 +136,21 @@ fn test_clustering_serialization() { fn test_clustering_no_solution() { // 3 elements all pairwise distance 5, K=1, B=2 → infeasible let distances = vec![vec![0, 5, 5], vec![5, 0, 5], vec![5, 5, 0]]; - let problem = Clustering::new(distances, 1, 2); + let problem = Clustering::new(distances, 1, 2).unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); } #[test] -#[should_panic(expected = "symmetric")] -fn test_clustering_asymmetric_panics() { +fn test_clustering_asymmetric_is_rejected() { let distances = vec![vec![0, 1], vec![2, 0]]; - Clustering::new(distances, 1, 1); + assert!(Clustering::new(distances, 1, 1).is_err()); } #[test] -#[should_panic(expected = "Diagonal")] -fn test_clustering_nonzero_diagonal_panics() { +fn test_clustering_nonzero_diagonal_is_rejected() { let distances = vec![vec![1, 1], vec![1, 0]]; - Clustering::new(distances, 1, 1); + assert!(Clustering::new(distances, 1, 1).is_err()); } #[test] diff --git a/src/unit_tests/models/misc/conjunctive_boolean_query.rs b/src/unit_tests/models/misc/conjunctive_boolean_query.rs index 165eb95a0..ad32c50f8 100644 --- a/src/unit_tests/models/misc/conjunctive_boolean_query.rs +++ b/src/unit_tests/models/misc/conjunctive_boolean_query.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; /// Helper to build the issue example instance. @@ -27,7 +26,7 @@ fn issue_example() -> ConjunctiveBooleanQuery { ], ), ]; - ConjunctiveBooleanQuery::new(6, relations, 2, conjuncts) + ConjunctiveBooleanQuery::new(6, relations, 2, conjuncts).unwrap() } #[test] @@ -37,7 +36,10 @@ fn test_conjunctivebooleanquery_basic() { assert_eq!(problem.num_relations(), 2); assert_eq!(problem.num_variables(), 2); assert_eq!(problem.num_conjuncts(), 3); - assert_eq!(problem.dimensions(), vec![6, 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6, 6] + ); assert_eq!( ::NAME, "ConjunctiveBooleanQuery" @@ -114,7 +116,7 @@ fn test_conjunctivebooleanquery_unsatisfiable() { (0, vec![QueryArg::Variable(0), QueryArg::Variable(0)]), (0, vec![QueryArg::Variable(0), QueryArg::Constant(1)]), ]; - let problem = ConjunctiveBooleanQuery::new(2, relations, 1, conjuncts); + let problem = ConjunctiveBooleanQuery::new(2, relations, 1, conjuncts).unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); } diff --git a/src/unit_tests/models/misc/conjunctive_query_foldability.rs b/src/unit_tests/models/misc/conjunctive_query_foldability.rs index 29fc69f39..e9ed23a9e 100644 --- a/src/unit_tests/models/misc/conjunctive_query_foldability.rs +++ b/src/unit_tests/models/misc/conjunctive_query_foldability.rs @@ -30,6 +30,7 @@ fn yes_instance() -> ConjunctiveQueryFoldability { (0, vec![U(2), X(0)]), ], ) + .unwrap() } /// Build the NO instance (not foldable): @@ -52,6 +53,7 @@ fn no_instance() -> ConjunctiveQueryFoldability { // Q2: R(x,a) ∧ R(a,x) vec![(0, vec![X(0), U(2)]), (0, vec![U(2), X(0)])], ) + .unwrap() } #[test] @@ -59,8 +61,11 @@ fn test_conjunctive_query_foldability_creation() { let problem = yes_instance(); // dims = [domain_size + num_distinguished + num_undistinguished; num_undistinguished] // = [0 + 1 + 3; 3] = [4, 4, 4] - assert_eq!(problem.dimensions(), vec![4, 4, 4]); - assert_eq!(problem.num_variables(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 4, 4] + ); + assert_eq!(problem.num_variables().unwrap(), 3); assert_eq!( ::NAME, "ConjunctiveQueryFoldability" @@ -117,7 +122,10 @@ fn test_conjunctive_query_foldability_serialization() { let problem = yes_instance(); let json = serde_json::to_value(&problem).unwrap(); let restored: ConjunctiveQueryFoldability = serde_json::from_value(json).unwrap(); - assert_eq!(restored.dimensions(), problem.dimensions()); + assert_eq!( + crate::solvers::cartesian_dimensions(&restored).unwrap(), + crate::solvers::cartesian_dimensions(&problem).unwrap() + ); assert_eq!(restored.domain_size(), problem.domain_size()); assert_eq!(restored.num_distinguished(), problem.num_distinguished()); assert_eq!( @@ -172,9 +180,13 @@ fn test_conjunctive_query_foldability_with_constants() { (0, vec![C(0), X(0)]), // R(c0, x) (0, vec![X(0), X(0)]), // R(x, x) ], - ); + ) + .unwrap(); // dims = [1+1+1; 1] = [3] - assert_eq!(problem.dimensions(), vec![3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3] + ); // σ(u→x): index for X(0) = domain_size + 0 = 1 assert!(problem.evaluate(&vec![1]).unwrap()); // σ(u→c0): index for C(0) = 0 → R(c0, c0) ∧ R(c0, x) ≠ Q2 @@ -219,59 +231,59 @@ fn test_conjunctive_query_foldability_evaluate_out_of_range() { } #[test] -#[should_panic(expected = "relation index")] fn test_conjunctive_query_foldability_bad_relation_index() { use Term::Distinguished as X; - ConjunctiveQueryFoldability::new( + assert!(ConjunctiveQueryFoldability::new( 0, 1, 0, vec![2], vec![(5, vec![X(0), X(0)])], // relation 5 doesn't exist vec![], - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "arity")] fn test_conjunctive_query_foldability_bad_arity() { use Term::Distinguished as X; - ConjunctiveQueryFoldability::new( + assert!(ConjunctiveQueryFoldability::new( 0, 1, 0, vec![2], vec![(0, vec![X(0)])], // arity 2 but 1 arg vec![], - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "Distinguished")] fn test_conjunctive_query_foldability_bad_distinguished() { use Term::Distinguished as X; - ConjunctiveQueryFoldability::new( + assert!(ConjunctiveQueryFoldability::new( 0, 1, 0, vec![2], vec![(0, vec![X(0), X(1)])], // X(1) out of range vec![], - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "Constant")] fn test_conjunctive_query_foldability_bad_constant() { use Term::{Constant as C, Distinguished as X}; - ConjunctiveQueryFoldability::new( + assert!(ConjunctiveQueryFoldability::new( 1, 1, 0, vec![2], vec![(0, vec![X(0), C(1)])], // C(1) out of range for domain_size=1 vec![], - ); + ) + .is_err()); } #[test] @@ -285,8 +297,12 @@ fn test_conjunctive_query_foldability_no_undistinguished() { vec![2], vec![(0, vec![X(0), X(0)])], vec![(0, vec![X(0), X(0)])], + ) + .unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() ); - assert_eq!(problem.dimensions(), Vec::::new()); assert!(problem.evaluate(&vec![]).unwrap()); } @@ -301,20 +317,21 @@ fn test_conjunctive_query_foldability_no_undistinguished_not_equal() { vec![2], vec![(0, vec![X(0), X(1)])], vec![(0, vec![X(1), X(0)])], - ); + ) + .unwrap(); assert!(!problem.evaluate(&vec![]).unwrap()); } #[test] -#[should_panic(expected = "Undistinguished")] fn test_conjunctive_query_foldability_bad_undistinguished() { use Term::{Distinguished as X, Undistinguished as U}; - ConjunctiveQueryFoldability::new( + assert!(ConjunctiveQueryFoldability::new( 0, 1, 1, vec![2], vec![(0, vec![X(0), U(1)])], // U(1) out of range for num_undistinguished=1 vec![], - ); + ) + .is_err()); } 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 1aadc7b88..e2ce48316 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,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_known_values() { @@ -31,6 +30,7 @@ fn issue_yes_instance() -> ConsistencyOfDatabaseFrequencyTables { KnownValue::new(1, 2, 1), ], ) + .unwrap() } fn issue_yes_witness() -> Vec { @@ -44,6 +44,7 @@ fn small_yes_instance() -> ConsistencyOfDatabaseFrequencyTables { vec![FrequencyTable::new(0, 1, vec![vec![1, 0], vec![0, 1]])], vec![KnownValue::new(0, 0, 0)], ) + .unwrap() } fn small_no_instance() -> ConsistencyOfDatabaseFrequencyTables { @@ -53,6 +54,7 @@ fn small_no_instance() -> ConsistencyOfDatabaseFrequencyTables { vec![FrequencyTable::new(0, 1, vec![vec![1, 0], vec![0, 1]])], vec![KnownValue::new(0, 0, 0), KnownValue::new(1, 1, 0)], ) + .unwrap() } #[test] @@ -60,7 +62,7 @@ fn test_cdft_creation_and_getters() { let problem = issue_yes_instance(); assert_eq!(problem.num_objects(), 6); assert_eq!(problem.num_attributes(), 3); - assert_eq!(problem.domain_size_product(), 12); + assert_eq!(problem.max_domain_size(), 3); assert_eq!(problem.num_assignment_variables(), 18); assert_eq!(problem.attribute_domains(), &[2, 3, 2]); assert_eq!(problem.frequency_tables().len(), 2); @@ -79,7 +81,7 @@ fn test_cdft_creation_and_getters() { fn test_cdft_dims_repeat_attribute_domains_for_each_object() { let problem = issue_yes_instance(); assert_eq!( - problem.dimensions(), + crate::solvers::cartesian_dimensions(&problem).unwrap(), vec![2, 3, 2, 2, 3, 2, 2, 3, 2, 2, 3, 2, 2, 3, 2, 2, 3, 2] ); } @@ -169,23 +171,23 @@ fn test_cdft_paper_example_matches_issue_witness() { } #[test] -#[should_panic(expected = "frequency table rows")] fn test_cdft_constructor_rejects_wrong_row_count() { - let _ = ConsistencyOfDatabaseFrequencyTables::new( + assert!(ConsistencyOfDatabaseFrequencyTables::new( 2, vec![2, 2], vec![FrequencyTable::new(0, 1, vec![vec![1, 0]])], vec![], - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "known value value")] fn test_cdft_constructor_rejects_out_of_range_known_value() { - let _ = ConsistencyOfDatabaseFrequencyTables::new( + assert!(ConsistencyOfDatabaseFrequencyTables::new( 2, vec![2, 2], vec![FrequencyTable::new(0, 1, vec![vec![1, 0], vec![0, 1]])], vec![KnownValue::new(0, 1, 2)], - ); + ) + .is_err()); } diff --git a/src/unit_tests/models/misc/cosine_product_integration.rs b/src/unit_tests/models/misc/cosine_product_integration.rs index 5787b6f66..004ae99d1 100644 --- a/src/unit_tests/models/misc/cosine_product_integration.rs +++ b/src/unit_tests/models/misc/cosine_product_integration.rs @@ -1,46 +1,48 @@ use crate::models::misc::CosineProductIntegration; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] fn test_cosine_product_integration_creation() { - let p = CosineProductIntegration::new(vec![2, 3, 5]); + let p = CosineProductIntegration::new(vec![2, 3, 5]).unwrap(); assert_eq!(p.coefficients(), &[2, 3, 5]); assert_eq!(p.num_coefficients(), 3); } #[test] fn test_cosine_product_integration_dims() { - let p = CosineProductIntegration::new(vec![1, 2, 3]); - assert_eq!(p.dimensions(), vec![2, 2, 2]); + let p = CosineProductIntegration::new(vec![1, 2, 3]).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] + ); } #[test] fn test_cosine_product_integration_evaluate_satisfying() { // [2, 3, 5]: (+2, +3, -5) = 0 → satisfying - let p = CosineProductIntegration::new(vec![2, 3, 5]); + let p = CosineProductIntegration::new(vec![2, 3, 5]).unwrap(); assert!(p.evaluate(&vec![false, false, true]).unwrap().0); } #[test] fn test_cosine_product_integration_evaluate_not_satisfying() { // [2, 3, 5]: (+2, +3, +5) = 10 → not satisfying - let p = CosineProductIntegration::new(vec![2, 3, 5]); + let p = CosineProductIntegration::new(vec![2, 3, 5]).unwrap(); assert!(!p.evaluate(&vec![false, false, false]).unwrap().0); } #[test] fn test_cosine_product_integration_unsatisfiable() { // [1, 2, 6]: total=9 (odd), no balanced sign assignment - let p = CosineProductIntegration::new(vec![1, 2, 6]); + let p = CosineProductIntegration::new(vec![1, 2, 6]).unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&p).unwrap().is_none()); } #[test] fn test_cosine_product_integration_solver() { - let p = CosineProductIntegration::new(vec![2, 3, 5]); + let p = CosineProductIntegration::new(vec![2, 3, 5]).unwrap(); let solver = BruteForce::new(); let witness = solver.solve(&p).unwrap().unwrap(); assert!(p.evaluate(&witness).unwrap().0); @@ -48,13 +50,13 @@ fn test_cosine_product_integration_solver() { #[test] fn test_cosine_product_integration_aggregate() { - let p = CosineProductIntegration::new(vec![2, 3, 5]); + let p = CosineProductIntegration::new(vec![2, 3, 5]).unwrap(); let solver = BruteForce::new(); let value_solution = solver.solve(&p).unwrap().unwrap(); let value = p.evaluate(&value_solution).unwrap(); assert!(value.0); - let p2 = CosineProductIntegration::new(vec![1, 2, 6]); + let p2 = CosineProductIntegration::new(vec![1, 2, 6]).unwrap(); let value2 = solver.solve(&p2).unwrap(); assert!(value2.is_none()); } @@ -63,13 +65,13 @@ fn test_cosine_product_integration_aggregate() { fn test_cosine_product_integration_negative_coefficients() { // [-3, 2, 1]: (-(-3), +2, -1) = (3, 2, -1) = 4, not zero // but (-3, +2, +1) = 0 → config [0, 0, 0] → -3+2+1=0 - let p = CosineProductIntegration::new(vec![-3, 2, 1]); + let p = CosineProductIntegration::new(vec![-3, 2, 1]).unwrap(); assert!(p.evaluate(&vec![false, false, false]).unwrap().0); // -3 + 2 + 1 = 0 } #[test] fn test_cosine_product_integration_invalid_config() { - let p = CosineProductIntegration::new(vec![1, 2, 3]); + let p = CosineProductIntegration::new(vec![1, 2, 3]).unwrap(); // Wrong length assert!(matches!( p.evaluate(&vec![false, false]), @@ -84,7 +86,7 @@ fn test_cosine_product_integration_invalid_config() { #[test] fn test_cosine_product_integration_serialization() { - let p = CosineProductIntegration::new(vec![2, 3, 5]); + let p = CosineProductIntegration::new(vec![2, 3, 5]).unwrap(); let json = serde_json::to_string(&p).unwrap(); let p2: CosineProductIntegration = serde_json::from_str(&json).unwrap(); assert_eq!(p2.coefficients(), p.coefficients()); @@ -93,7 +95,7 @@ fn test_cosine_product_integration_serialization() { #[test] fn test_cosine_product_integration_all_witnesses() { // [2, 3, 5]: two balanced assignments: (+2,+3,-5)=0 and (-2,-3,+5)=0 - let p = CosineProductIntegration::new(vec![2, 3, 5]); + let p = CosineProductIntegration::new(vec![2, 3, 5]).unwrap(); let solver = BruteForce::new(); let witnesses = solver.find_all_witnesses(&p).unwrap(); assert_eq!(witnesses.len(), 2); @@ -103,7 +105,6 @@ fn test_cosine_product_integration_all_witnesses() { } #[test] -#[should_panic] fn test_cosine_product_integration_empty() { - CosineProductIntegration::new(vec![]); + assert!(CosineProductIntegration::new(vec![]).is_err()); } diff --git a/src/unit_tests/models/misc/cyclic_ordering.rs b/src/unit_tests/models/misc/cyclic_ordering.rs index 5f8bb0b76..20eaaaee4 100644 --- a/src/unit_tests/models/misc/cyclic_ordering.rs +++ b/src/unit_tests/models/misc/cyclic_ordering.rs @@ -14,8 +14,11 @@ fn test_cyclic_ordering_basic() { assert_eq!(problem.num_elements(), 5); assert_eq!(problem.num_triples(), 3); assert_eq!(problem.triples(), &[(0, 1, 2), (2, 3, 0), (1, 3, 4)]); - assert_eq!(problem.dimensions(), vec![5; 5]); - assert_eq!(problem.num_variables(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 5] + ); + assert_eq!(problem.num_variables().unwrap(), 5); assert_eq!(::NAME, "CyclicOrdering"); assert_eq!(::variant(), vec![]); } diff --git a/src/unit_tests/models/misc/dynamic_storage_allocation.rs b/src/unit_tests/models/misc/dynamic_storage_allocation.rs index 42fd55c8c..79f463dc7 100644 --- a/src/unit_tests/models/misc/dynamic_storage_allocation.rs +++ b/src/unit_tests/models/misc/dynamic_storage_allocation.rs @@ -20,8 +20,11 @@ fn test_dynamic_storage_allocation_basic() { assert_eq!(problem.items().len(), 5); // dims: D - s(a) + 1 for each item // sizes are 2, 3, 1, 3, 2 => dims are 5, 4, 6, 4, 5 - assert_eq!(problem.dimensions(), vec![5, 4, 6, 4, 5]); - assert_eq!(problem.num_variables(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 4, 6, 4, 5] + ); + assert_eq!(problem.num_variables().unwrap(), 5); assert_eq!( ::NAME, "DynamicStorageAllocation" diff --git a/src/unit_tests/models/misc/ensemble_computation.rs b/src/unit_tests/models/misc/ensemble_computation.rs index 890207764..89e53816c 100644 --- a/src/unit_tests/models/misc/ensemble_computation.rs +++ b/src/unit_tests/models/misc/ensemble_computation.rs @@ -15,8 +15,11 @@ fn test_ensemble_computation_creation() { assert_eq!(problem.universe_size(), 4); assert_eq!(problem.num_subsets(), 2); assert_eq!(problem.budget(), 4); - assert_eq!(problem.num_variables(), 8); - assert_eq!(problem.dimensions(), vec![8; 8]); + assert_eq!(problem.num_variables().unwrap(), 8); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![8; 8] + ); assert_eq!( ::NAME, "EnsembleComputation" diff --git a/src/unit_tests/models/misc/expected_retrieval_cost.rs b/src/unit_tests/models/misc/expected_retrieval_cost.rs index 9cd84b62c..f2172769a 100644 --- a/src/unit_tests/models/misc/expected_retrieval_cost.rs +++ b/src/unit_tests/models/misc/expected_retrieval_cost.rs @@ -16,8 +16,11 @@ fn test_expected_retrieval_cost_basic_accessors() { assert_eq!(problem.num_records(), 6); assert_eq!(problem.num_sectors(), 3); assert_eq!(problem.probabilities(), &[0.2, 0.15, 0.15, 0.2, 0.1, 0.2]); - assert_eq!(problem.dimensions(), vec![3; 6]); - assert_eq!(problem.num_variables(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 6] + ); + assert_eq!(problem.num_variables().unwrap(), 6); } #[test] diff --git a/src/unit_tests/models/misc/factoring.rs b/src/unit_tests/models/misc/factoring.rs index 61bf2f201..3091fa794 100644 --- a/src/unit_tests/models/misc/factoring.rs +++ b/src/unit_tests/models/misc/factoring.rs @@ -1,9 +1,9 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use num_bigint::BigUint; -include!("../../jl_helpers.rs"); #[test] fn test_factoring_creation() { @@ -11,7 +11,7 @@ fn test_factoring_creation() { assert_eq!(problem.m(), 3); assert_eq!(problem.n(), 3); assert_eq!(problem.target(), &BigUint::from(15u32)); - assert_eq!(problem.num_variables(), 6); + assert_eq!(problem.num_variables().unwrap(), 6); } #[test] @@ -136,7 +136,7 @@ fn test_parameter_getters() { fn test_factoring_paper_example() { // Paper: N=15, m=2 bits, n=3 bits, p=3, q=5 let problem = Factoring::with_factor_bits(15, 2, 3); - assert_eq!(problem.num_variables(), 5); + assert_eq!(problem.num_variables().unwrap(), 5); // p=3 -> bits [1,1], q=5 -> bits [1,0,1] let config = (BigUint::from(3u32), BigUint::from(5u32)); diff --git a/src/unit_tests/models/misc/feasible_register_assignment.rs b/src/unit_tests/models/misc/feasible_register_assignment.rs index 08b10831f..54e2d0e66 100644 --- a/src/unit_tests/models/misc/feasible_register_assignment.rs +++ b/src/unit_tests/models/misc/feasible_register_assignment.rs @@ -1,19 +1,22 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] fn test_feasible_register_assignment_basic() { let problem = - FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]); + FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]) + .unwrap(); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_arcs(), 3); assert_eq!(problem.num_registers(), 2); assert_eq!(problem.num_same_register_pairs(), 3); assert_eq!(problem.arcs(), &[(0, 1), (0, 2), (1, 3)]); assert_eq!(problem.assignment(), &[0, 1, 0, 0]); - assert_eq!(problem.dimensions(), vec![4; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 4] + ); assert_eq!( ::NAME, "FeasibleRegisterAssignment" @@ -28,7 +31,8 @@ fn test_feasible_register_assignment_evaluate_valid() { // Order: v3(pos0), v1(pos1), v2(pos2), v0(pos3) // config[v] = position let problem = - FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]); + FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]) + .unwrap(); let config = vec![3, 1, 2, 0]; assert!(problem.evaluate(&config).unwrap()); } @@ -36,7 +40,8 @@ fn test_feasible_register_assignment_evaluate_valid() { #[test] fn test_feasible_register_assignment_evaluate_invalid_permutation() { let problem = - FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]); + FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]) + .unwrap(); // Not a permutation: position 0 used twice assert!(!problem.evaluate(&vec![0, 0, 1, 2]).unwrap()); // Wrong length @@ -59,7 +64,8 @@ fn test_feasible_register_assignment_evaluate_invalid_permutation() { fn test_feasible_register_assignment_evaluate_invalid_dependency() { // v0 depends on v1, v1 depends on v3 let problem = - FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]); + FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]) + .unwrap(); // v0 at position 0 but v1 at position 1 -> v0 evaluated before its dependency v1 assert!(!problem.evaluate(&vec![0, 1, 2, 3]).unwrap()); } @@ -71,14 +77,16 @@ fn test_feasible_register_assignment_register_conflict() { // In any valid topological order, v1 must come first. // After computing v1 (reg 0), v1 is live until both v0 and v2 are computed. // Computing v0 or v2 next would need register 0, but v1 is still live there. - let problem = FeasibleRegisterAssignment::new(3, vec![(0, 1), (2, 1)], 2, vec![0, 0, 0]); + let problem = + FeasibleRegisterAssignment::new(3, vec![(0, 1), (2, 1)], 2, vec![0, 0, 0]).unwrap(); // v1 at pos 0, v0 at pos 1, v2 at pos 2 // After computing v1 (reg 0): v1 is live (v0, v2 still uncomputed) // Computing v0 (reg 0): conflict! v1 is still live in reg 0 assert!(!problem.evaluate(&vec![1, 0, 2]).unwrap()); // With different assignment: v1->reg 1, v0->reg 0, v2->reg 0 - let problem2 = FeasibleRegisterAssignment::new(3, vec![(0, 1), (2, 1)], 2, vec![0, 1, 0]); + let problem2 = + FeasibleRegisterAssignment::new(3, vec![(0, 1), (2, 1)], 2, vec![0, 1, 0]).unwrap(); // v1 at pos 0, v0 at pos 1, v2 at pos 2 // After computing v1 (reg 1): v1 is live // Computing v0 (reg 0): no conflict, v0 uses reg 0 @@ -91,7 +99,8 @@ fn test_feasible_register_assignment_register_conflict() { #[test] fn test_feasible_register_assignment_brute_force() { let problem = - FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]); + FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]) + .unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -103,7 +112,8 @@ fn test_feasible_register_assignment_brute_force() { #[test] fn test_feasible_register_assignment_brute_force_all() { let problem = - FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]); + FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -121,7 +131,7 @@ fn test_feasible_register_assignment_unsatisfiable() { // and v2 has uncomputed dependent v0 (excluding v1), so v2 is live. // Computing v1 in reg 0 conflicts with live v2. let problem = - FeasibleRegisterAssignment::new(3, vec![(0, 1), (0, 2), (1, 2)], 1, vec![0, 0, 0]); + FeasibleRegisterAssignment::new(3, vec![(0, 1), (0, 2), (1, 2)], 1, vec![0, 0, 0]).unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); } @@ -129,7 +139,8 @@ fn test_feasible_register_assignment_unsatisfiable() { #[test] fn test_feasible_register_assignment_serialization() { let problem = - FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]); + FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]) + .unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: FeasibleRegisterAssignment = serde_json::from_value(json).unwrap(); assert_eq!(restored.num_vertices(), problem.num_vertices()); @@ -141,15 +152,18 @@ fn test_feasible_register_assignment_serialization() { #[test] fn test_feasible_register_assignment_empty() { - let problem = FeasibleRegisterAssignment::new(0, vec![], 0, vec![]); + let problem = FeasibleRegisterAssignment::new(0, vec![], 0, vec![]).unwrap(); assert_eq!(problem.num_vertices(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } #[test] fn test_feasible_register_assignment_single_vertex() { - let problem = FeasibleRegisterAssignment::new(1, vec![], 1, vec![0]); + let problem = FeasibleRegisterAssignment::new(1, vec![], 1, vec![0]).unwrap(); assert!(problem.evaluate(&vec![0]).unwrap()); } @@ -159,7 +173,7 @@ fn test_feasible_register_assignment_no_dependencies() { // Any permutation is valid as long as no register conflict. // v0(reg 0) and v2(reg 0): since there are no dependencies, no vertex is // ever "live" (no dependents), so no conflicts can arise. - let problem = FeasibleRegisterAssignment::new(3, vec![], 2, vec![0, 1, 0]); + let problem = FeasibleRegisterAssignment::new(3, vec![], 2, vec![0, 1, 0]).unwrap(); // Any order works since no vertex has dependents => nothing is ever live assert!(problem.evaluate(&vec![0, 1, 2]).unwrap()); assert!(problem.evaluate(&vec![2, 1, 0]).unwrap()); @@ -167,6 +181,18 @@ fn test_feasible_register_assignment_no_dependencies() { #[test] fn test_feasible_register_assignment_same_register_pair_count() { - let problem = FeasibleRegisterAssignment::new(5, vec![], 3, vec![0, 1, 0, 2, 0]); + let problem = FeasibleRegisterAssignment::new(5, vec![], 3, vec![0, 1, 0, 2, 0]).unwrap(); assert_eq!(problem.num_same_register_pairs(), 3); } + +#[test] +fn deserialize_rejects_invalid_indices_before_building_adjacency() { + for (arcs, assignment) in [(vec![(0, 2)], vec![0, 0]), (vec![(0, 1)], vec![0, 1])] { + assert!( + serde_json::from_value::(serde_json::json!({ + "num_vertices": 2, "arcs": arcs, "num_registers": 1, "assignment": assignment + })) + .is_err() + ); + } +} diff --git a/src/unit_tests/models/misc/flow_shop_scheduling.rs b/src/unit_tests/models/misc/flow_shop_scheduling.rs index a3b327622..b35008154 100644 --- a/src/unit_tests/models/misc/flow_shop_scheduling.rs +++ b/src/unit_tests/models/misc/flow_shop_scheduling.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -15,13 +14,22 @@ fn test_flow_shop_scheduling_creation() { vec![3, 2, 3], ], 25, - ); + ) + .unwrap(); assert_eq!(problem.num_jobs(), 5); assert_eq!(problem.num_processors(), 3); assert_eq!(problem.deadline(), 25); - assert_eq!(problem.dimensions().len(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 5 + ); // Lehmer code encoding: dims = [5, 4, 3, 2, 1] - assert_eq!(problem.dimensions(), vec![5, 4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 4, 3, 2, 1] + ); } #[test] @@ -40,7 +48,8 @@ fn test_flow_shop_scheduling_evaluate_feasible() { vec![3, 2, 3], ], 25, - ); + ) + .unwrap(); let config = vec![3, 0, 4, 2, 1]; assert!(problem.evaluate(&config).unwrap()); @@ -59,7 +68,8 @@ fn test_flow_shop_scheduling_evaluate_infeasible() { vec![3, 2, 3], ], 15, // Very tight deadline, likely infeasible - ); + ) + .unwrap(); // The sequence j4,j1,j5,j3,j2 gives makespan 23 > 15 let config = vec![3, 0, 4, 2, 1]; @@ -68,7 +78,7 @@ fn test_flow_shop_scheduling_evaluate_infeasible() { #[test] fn test_flow_shop_scheduling_invalid_config() { - let problem = FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10); + let problem = FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10).unwrap(); assert!(matches!( problem.evaluate(&vec![2, 0]), @@ -99,7 +109,7 @@ fn test_flow_shop_scheduling_variant() { #[test] fn test_flow_shop_scheduling_serialization() { - let problem = FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4], vec![2, 1]], 10); + let problem = FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4], vec![2, 1]], 10).unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: FlowShopScheduling = serde_json::from_value(json).unwrap(); assert_eq!(restored.num_processors(), problem.num_processors()); @@ -111,7 +121,7 @@ fn test_flow_shop_scheduling_serialization() { fn test_flow_shop_scheduling_compute_makespan() { // 2 machines, 3 jobs // Job 0: [3, 2], Job 1: [2, 4], Job 2: [1, 3] - let problem = FlowShopScheduling::new(2, vec![vec![3, 2], vec![2, 4], vec![1, 3]], 20); + let problem = FlowShopScheduling::new(2, vec![vec![3, 2], vec![2, 4], vec![1, 3]], 20).unwrap(); // Order: job 0, job 1, job 2 // Machine 0: j0[0,3], j1[3,5], j2[5,6] @@ -123,7 +133,7 @@ fn test_flow_shop_scheduling_compute_makespan() { #[test] 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 problem = FlowShopScheduling::new(2, vec![vec![3, 2], vec![2, 4], vec![1, 3]], 20).unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); assert!(solution.is_some()); @@ -139,7 +149,7 @@ fn test_flow_shop_scheduling_brute_force_unsatisfiable() { // [0,1]: M0: 0-5, 5-10; M1: 5-10, 10-15 -> 15 // [1,0]: same by symmetry -> 15 // Deadline 10 < 15 => unsatisfiable - let problem = FlowShopScheduling::new(2, vec![vec![5, 5], vec![5, 5]], 10); + let problem = FlowShopScheduling::new(2, vec![vec![5, 5], vec![5, 5]], 10).unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); assert!(solution.is_none()); @@ -147,9 +157,12 @@ fn test_flow_shop_scheduling_brute_force_unsatisfiable() { #[test] fn test_flow_shop_scheduling_empty() { - let problem = FlowShopScheduling::new(3, vec![], 0); + let problem = FlowShopScheduling::new(3, vec![], 0).unwrap(); assert_eq!(problem.num_jobs(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); // Empty config should be satisfying (no jobs to schedule) assert!(problem.evaluate(&vec![]).unwrap()); } @@ -168,7 +181,8 @@ fn test_flow_shop_scheduling_find_all_witnesses() { vec![3, 2, 3], ], 25, - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); for sol in &solutions { @@ -184,7 +198,7 @@ fn test_flow_shop_scheduling_find_all_witnesses() { 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 problem = FlowShopScheduling::new(2, vec![vec![5, 5], vec![5, 5]], 10).unwrap(); let solver = BruteForce::new(); assert!(solver.find_all_witnesses(&problem).unwrap().is_empty()); } @@ -193,8 +207,8 @@ fn test_flow_shop_scheduling_find_all_witnesses_empty() { fn test_flow_shop_scheduling_single_job() { // 3 machines, 1 job: [2, 3, 4] // Makespan = 2 + 3 + 4 = 9 - let problem = FlowShopScheduling::new(3, vec![vec![2, 3, 4]], 10); + let problem = FlowShopScheduling::new(3, vec![vec![2, 3, 4]], 10).unwrap(); assert!(problem.evaluate(&vec![0]).unwrap()); // makespan 9 <= 10 - let tight = FlowShopScheduling::new(3, vec![vec![2, 3, 4]], 8); + let tight = FlowShopScheduling::new(3, vec![vec![2, 3, 4]], 8).unwrap(); assert!(!tight.evaluate(&vec![0]).unwrap()); // makespan 9 > 8 } diff --git a/src/unit_tests/models/misc/grouping_by_swapping.rs b/src/unit_tests/models/misc/grouping_by_swapping.rs index 7b56a31d8..904c7d915 100644 --- a/src/unit_tests/models/misc/grouping_by_swapping.rs +++ b/src/unit_tests/models/misc/grouping_by_swapping.rs @@ -4,15 +4,15 @@ use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; fn issue_yes_instance() -> GroupingBySwapping { - GroupingBySwapping::new(3, vec![0, 1, 2, 0, 1, 2], 5) + GroupingBySwapping::new(3, vec![0, 1, 2, 0, 1, 2], 5).unwrap() } fn issue_minimum_three_swaps_instance() -> GroupingBySwapping { - GroupingBySwapping::new(3, vec![0, 1, 2, 0, 1, 2], 3) + GroupingBySwapping::new(3, vec![0, 1, 2, 0, 1, 2], 3).unwrap() } fn issue_two_swap_instance() -> GroupingBySwapping { - GroupingBySwapping::new(3, vec![0, 1, 2, 0, 1, 2], 2) + GroupingBySwapping::new(3, vec![0, 1, 2, 0, 1, 2], 2).unwrap() } #[test] @@ -22,8 +22,11 @@ fn test_grouping_by_swapping_basic() { assert_eq!(problem.string(), &[0, 1, 2, 0, 1, 2]); assert_eq!(problem.budget(), 5); assert_eq!(problem.string_len(), 6); - assert_eq!(problem.num_variables(), 5); - assert_eq!(problem.dimensions(), vec![6; 5]); + assert_eq!(problem.num_variables().unwrap(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6; 5] + ); assert_eq!(::NAME, "GroupingBySwapping"); assert_eq!(::variant(), vec![]); @@ -106,15 +109,13 @@ fn test_grouping_by_swapping_serialization() { } #[test] -#[should_panic(expected = "input symbols must be less than alphabet_size")] -fn test_grouping_by_swapping_symbol_out_of_range_panics() { - GroupingBySwapping::new(3, vec![0, 1, 3], 1); +fn test_grouping_by_swapping_symbol_out_of_range_is_rejected() { + assert!(GroupingBySwapping::new(3, vec![0, 1, 3], 1).is_err()); } #[test] -#[should_panic(expected = "budget must be 0 when string is empty")] fn test_grouping_by_swapping_empty_string_requires_zero_budget() { - GroupingBySwapping::new(0, vec![], 1); + assert!(GroupingBySwapping::new(0, vec![], 1).is_err()); } #[test] diff --git a/src/unit_tests/models/misc/integer_expression_membership.rs b/src/unit_tests/models/misc/integer_expression_membership.rs index 781cdab77..cec269f66 100644 --- a/src/unit_tests/models/misc/integer_expression_membership.rs +++ b/src/unit_tests/models/misc/integer_expression_membership.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; /// Helper: build expression (1 ∪ 4) + (3 ∪ 6) + (2 ∪ 5) @@ -26,13 +25,16 @@ fn example_expr() -> IntExpr { #[test] fn test_integer_expression_membership_creation() { let expr = example_expr(); - let problem = IntegerExpressionMembership::new(expr.clone(), 12); + let problem = IntegerExpressionMembership::new(expr.clone(), 12).unwrap(); assert_eq!(problem.target(), 12); assert_eq!(problem.num_union_nodes(), 3); assert_eq!(problem.num_atoms(), 6); assert_eq!(problem.expression_size(), 11); // 6 atoms + 3 unions + 2 sums assert_eq!(problem.expression_depth(), 3); - assert_eq!(problem.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2] + ); assert_eq!( ::NAME, "IntegerExpressionMembership" @@ -42,7 +44,7 @@ fn test_integer_expression_membership_creation() { #[test] fn test_integer_expression_membership_evaluate_satisfying() { - let problem = IntegerExpressionMembership::new(example_expr(), 12); + let problem = IntegerExpressionMembership::new(example_expr(), 12).unwrap(); // config [1,1,0]: choose 4, 6, 2 → 4+6+2=12 assert!(problem.evaluate(&vec![true, true, false]).unwrap()); // config [0,1,1]: choose 1, 6, 5 → 1+6+5=12 @@ -51,7 +53,7 @@ fn test_integer_expression_membership_evaluate_satisfying() { #[test] fn test_integer_expression_membership_evaluate_unsatisfying() { - let problem = IntegerExpressionMembership::new(example_expr(), 12); + let problem = IntegerExpressionMembership::new(example_expr(), 12).unwrap(); // config [0,0,0]: choose 1, 3, 2 → 1+3+2=6 ≠ 12 assert!(!problem.evaluate(&vec![false, false, false]).unwrap()); // config [1,0,0]: choose 4, 3, 2 → 4+3+2=9 ≠ 12 @@ -62,7 +64,7 @@ fn test_integer_expression_membership_evaluate_unsatisfying() { #[test] fn test_integer_expression_membership_evaluate_wrong_config() { - let problem = IntegerExpressionMembership::new(example_expr(), 12); + let problem = IntegerExpressionMembership::new(example_expr(), 12).unwrap(); // Wrong length assert!(matches!( problem.evaluate(&vec![false, false]), @@ -82,7 +84,7 @@ fn test_integer_expression_membership_evaluate_wrong_config() { #[test] fn test_integer_expression_membership_brute_force() { - let problem = IntegerExpressionMembership::new(example_expr(), 12); + let problem = IntegerExpressionMembership::new(example_expr(), 12).unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -93,7 +95,7 @@ fn test_integer_expression_membership_brute_force() { #[test] fn test_integer_expression_membership_brute_force_all() { - let problem = IntegerExpressionMembership::new(example_expr(), 12); + let problem = IntegerExpressionMembership::new(example_expr(), 12).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); // K=12 can be reached by [0,1,1] (1+6+5), [1,0,1] (4+3+5), [1,1,0] (4+6+2) @@ -106,7 +108,7 @@ fn test_integer_expression_membership_brute_force_all() { #[test] fn test_integer_expression_membership_unsatisfiable() { // Target 100 is unreachable from {1,4}+{3,6}+{2,5} (max is 15) - let problem = IntegerExpressionMembership::new(example_expr(), 100); + let problem = IntegerExpressionMembership::new(example_expr(), 100).unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); } @@ -114,16 +116,19 @@ fn test_integer_expression_membership_unsatisfiable() { #[test] fn test_integer_expression_membership_single_atom() { let expr = IntExpr::Atom(42); - let problem = IntegerExpressionMembership::new(expr, 42); + let problem = IntegerExpressionMembership::new(expr, 42).unwrap(); assert_eq!(problem.num_union_nodes(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); // empty config, atom == target } #[test] fn test_integer_expression_membership_single_atom_miss() { let expr = IntExpr::Atom(42); - let problem = IntegerExpressionMembership::new(expr, 7); + let problem = IntegerExpressionMembership::new(expr, 7).unwrap(); assert!(!problem.evaluate(&vec![]).unwrap()); // 42 ≠ 7 } @@ -131,9 +136,12 @@ fn test_integer_expression_membership_single_atom_miss() { fn test_integer_expression_membership_simple_union() { // (3 ∪ 7), target = 7 let expr = IntExpr::Union(Box::new(IntExpr::Atom(3)), Box::new(IntExpr::Atom(7))); - let problem = IntegerExpressionMembership::new(expr, 7); + let problem = IntegerExpressionMembership::new(expr, 7).unwrap(); assert_eq!(problem.num_union_nodes(), 1); - assert_eq!(problem.dimensions(), vec![2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2] + ); assert!(!problem.evaluate(&vec![false]).unwrap()); // 3 ≠ 7 assert!(problem.evaluate(&vec![true]).unwrap()); // 7 == 7 } @@ -142,7 +150,7 @@ fn test_integer_expression_membership_simple_union() { fn test_integer_expression_membership_simple_sum() { // Atom(3) + Atom(5), target = 8 let expr = IntExpr::Sum(Box::new(IntExpr::Atom(3)), Box::new(IntExpr::Atom(5))); - let problem = IntegerExpressionMembership::new(expr, 8); + let problem = IntegerExpressionMembership::new(expr, 8).unwrap(); assert_eq!(problem.num_union_nodes(), 0); assert!(problem.evaluate(&vec![]).unwrap()); // 3+5=8 } @@ -150,7 +158,7 @@ fn test_integer_expression_membership_simple_sum() { #[test] fn test_integer_expression_membership_serialization() { let expr = IntExpr::Union(Box::new(IntExpr::Atom(1)), Box::new(IntExpr::Atom(4))); - let problem = IntegerExpressionMembership::new(expr, 4); + let problem = IntegerExpressionMembership::new(expr, 4).unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: IntegerExpressionMembership = serde_json::from_value(json).unwrap(); assert_eq!(restored.target(), 4); @@ -160,7 +168,7 @@ fn test_integer_expression_membership_serialization() { #[test] fn test_integer_expression_membership_evaluate_config() { - let problem = IntegerExpressionMembership::new(example_expr(), 12); + let problem = IntegerExpressionMembership::new(example_expr(), 12).unwrap(); assert_eq!(problem.evaluate_config(&[true, true, false]), Some(12)); // 4+6+2 assert_eq!(problem.evaluate_config(&[false, false, false]), Some(6)); // 1+3+2 assert_eq!(problem.evaluate_config(&[true, true, true]), Some(15)); // 4+6+5 @@ -172,7 +180,7 @@ fn test_integer_expression_membership_paper_example() { // e = (1 ∪ 4) + (3 ∪ 6) + (2 ∪ 5), K = 12 // Set = {6, 9, 12, 15} // Witness: config [1, 1, 0] → 4+6+2 = 12 - let problem = IntegerExpressionMembership::new(example_expr(), 12); + let problem = IntegerExpressionMembership::new(example_expr(), 12).unwrap(); // Verify the claimed witness assert_eq!(problem.evaluate_config(&[true, true, false]), Some(12)); @@ -208,7 +216,7 @@ fn test_integer_expression_membership_nested_unions() { )), Box::new(IntExpr::Atom(3)), ); - let problem = IntegerExpressionMembership::new(expr, 2); + let problem = IntegerExpressionMembership::new(expr, 2).unwrap(); assert_eq!(problem.num_union_nodes(), 2); // DFS order: outer union (idx 0), inner union (idx 1) // [0, 0] → left of outer → left of inner → 1 @@ -227,21 +235,19 @@ fn test_integer_expression_membership_overflow_safe() { Box::new(IntExpr::Atom(i64::MAX)), Box::new(IntExpr::Atom(1)), ); - let problem = IntegerExpressionMembership::new(expr, 42); + let problem = IntegerExpressionMembership::new(expr, 42).unwrap(); // The only config is [] (no union nodes). The sum overflows → None → Or(false). assert!(!problem.evaluate(&vec![]).unwrap()); } #[test] -#[should_panic(expected = "all Atom values must be positive")] fn test_integer_expression_membership_zero_atom_rejected() { let expr = IntExpr::Atom(0); - IntegerExpressionMembership::new(expr, 1); + assert!(IntegerExpressionMembership::new(expr, 1).is_err()); } #[test] -#[should_panic(expected = "target must be a positive integer")] fn test_integer_expression_membership_zero_target_rejected() { let expr = IntExpr::Atom(1); - IntegerExpressionMembership::new(expr, 0); + assert!(IntegerExpressionMembership::new(expr, 0).is_err()); } diff --git a/src/unit_tests/models/misc/job_shop_scheduling.rs b/src/unit_tests/models/misc/job_shop_scheduling.rs index e190677ca..57921fd00 100644 --- a/src/unit_tests/models/misc/job_shop_scheduling.rs +++ b/src/unit_tests/models/misc/job_shop_scheduling.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -15,10 +14,11 @@ fn issue_example() -> JobShopScheduling { vec![(0, 2), (1, 3), (0, 1)], ], ) + .unwrap() } fn small_two_job_instance() -> JobShopScheduling { - JobShopScheduling::new(2, vec![vec![(0, 1), (1, 1)], vec![(1, 1), (0, 1)]]) + JobShopScheduling::new(2, vec![vec![(0, 1), (1, 1)], vec![(1, 1), (0, 1)]]).unwrap() } #[test] @@ -28,7 +28,7 @@ fn test_job_shop_scheduling_creation_and_dims() { assert_eq!(problem.num_jobs(), 5); assert_eq!(problem.num_tasks(), 12); assert_eq!( - problem.dimensions(), + crate::solvers::cartesian_dimensions(&problem).unwrap(), vec![6, 5, 4, 3, 2, 1, 6, 5, 4, 3, 2, 1] ); } diff --git a/src/unit_tests/models/misc/knapsack.rs b/src/unit_tests/models/misc/knapsack.rs index b58568b2f..546bc6027 100644 --- a/src/unit_tests/models/misc/knapsack.rs +++ b/src/unit_tests/models/misc/knapsack.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_item_weights() { @@ -16,19 +15,22 @@ use crate::traits::Problem; #[test] fn test_knapsack_basic() { - let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7).unwrap(); assert_eq!(problem.num_items(), 4); assert_eq!(problem.weights(), &[2, 3, 4, 5]); assert_eq!(problem.values(), &[3, 4, 5, 7]); assert_eq!(problem.capacity(), 7); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!(::NAME, "Knapsack"); assert_eq!(::variant(), vec![]); } #[test] fn test_knapsack_evaluate_optimal() { - let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7).unwrap(); assert_eq!( problem.evaluate(&vec![true, false, false, true]).unwrap(), Max(Some(10)) @@ -37,7 +39,7 @@ fn test_knapsack_evaluate_optimal() { #[test] fn test_knapsack_evaluate_feasible() { - let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7).unwrap(); assert_eq!( problem.evaluate(&vec![true, true, false, false]).unwrap(), Max(Some(7)) @@ -46,7 +48,7 @@ fn test_knapsack_evaluate_feasible() { #[test] fn test_knapsack_evaluate_overweight() { - let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7).unwrap(); assert_eq!( problem.evaluate(&vec![false, false, true, true]).unwrap(), Max(None) @@ -55,7 +57,7 @@ fn test_knapsack_evaluate_overweight() { #[test] fn test_knapsack_evaluate_empty() { - let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7).unwrap(); assert_eq!( problem.evaluate(&vec![false, false, false, false]).unwrap(), Max(Some(0)) @@ -64,7 +66,7 @@ fn test_knapsack_evaluate_empty() { #[test] fn test_knapsack_evaluate_all_selected() { - let problem = Knapsack::new(vec![1, 1, 1], vec![10, 20, 30], 5); + let problem = Knapsack::new(vec![1, 1, 1], vec![10, 20, 30], 5).unwrap(); assert_eq!( problem.evaluate(&vec![true, true, true]).unwrap(), Max(Some(60)) @@ -73,7 +75,7 @@ fn test_knapsack_evaluate_all_selected() { #[test] fn test_knapsack_evaluate_wrong_config_length() { - let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5); + let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5).unwrap(); assert!(matches!( problem.evaluate(&vec![true]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -86,7 +88,7 @@ fn test_knapsack_evaluate_wrong_config_length() { #[test] fn test_knapsack_evaluate_invalid_variable_value() { - let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5); + let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5).unwrap(); assert!( crate::registry::DynProblem::evaluate_dyn(&problem, &serde_json::json!([2, false])) .is_err() @@ -95,15 +97,18 @@ fn test_knapsack_evaluate_invalid_variable_value() { #[test] fn test_knapsack_empty_instance() { - let problem = Knapsack::new(vec![], vec![], 10); + let problem = Knapsack::new(vec![], vec![], 10).unwrap(); assert_eq!(problem.num_items(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), 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 problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7).unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -115,7 +120,7 @@ fn test_knapsack_brute_force() { #[test] fn test_knapsack_serialization() { - let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7).unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: Knapsack = serde_json::from_value(json).unwrap(); assert_eq!(restored.weights(), problem.weights()); @@ -126,7 +131,7 @@ fn test_knapsack_serialization() { #[test] fn test_knapsack_zero_capacity() { // Capacity 0: only empty set is feasible - let problem = Knapsack::new(vec![1, 2], vec![10, 20], 0); + let problem = Knapsack::new(vec![1, 2], vec![10, 20], 0).unwrap(); assert_eq!(problem.evaluate(&vec![false, false]).unwrap(), Max(Some(0))); assert_eq!(problem.evaluate(&vec![true, false]).unwrap(), Max(None)); let solver = BruteForce::new(); @@ -137,7 +142,7 @@ fn test_knapsack_zero_capacity() { #[test] fn test_knapsack_single_item() { // Single item that fits - let problem = Knapsack::new(vec![3], vec![5], 3); + let problem = Knapsack::new(vec![3], vec![5], 3).unwrap(); assert_eq!(problem.evaluate(&vec![true]).unwrap(), Max(Some(5))); assert_eq!(problem.evaluate(&vec![false]).unwrap(), Max(Some(0))); let solver = BruteForce::new(); @@ -152,34 +157,30 @@ fn test_knapsack_greedy_not_optimal() { // Item 1: w=5, v=5, ratio=1.00 // Item 2: w=5, v=5, ratio=1.00 // 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 problem = Knapsack::new(vec![6, 5, 5], vec![7, 5, 5], 10).unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&solution).unwrap(), Max(Some(10))); } #[test] -#[should_panic(expected = "weights and values must have the same length")] fn test_knapsack_mismatched_lengths() { - Knapsack::new(vec![1, 2], vec![3], 5); + assert!(Knapsack::new(vec![1, 2], vec![3], 5).is_err()); } #[test] -#[should_panic(expected = "nonnegative")] -fn test_knapsack_negative_weight_panics() { - Knapsack::new(vec![-1, 2], vec![3, 4], 5); +fn test_knapsack_negative_weight_is_rejected() { + assert!(Knapsack::new(vec![-1, 2], vec![3, 4], 5).is_err()); } #[test] -#[should_panic(expected = "nonnegative")] -fn test_knapsack_negative_value_panics() { - Knapsack::new(vec![1, 2], vec![-3, 4], 5); +fn test_knapsack_negative_value_is_rejected() { + assert!(Knapsack::new(vec![1, 2], vec![-3, 4], 5).is_err()); } #[test] -#[should_panic(expected = "nonnegative")] -fn test_knapsack_negative_capacity_panics() { - Knapsack::new(vec![1, 2], vec![3, 4], -1); +fn test_knapsack_negative_capacity_is_rejected() { + assert!(Knapsack::new(vec![1, 2], vec![3, 4], -1).is_err()); } #[test] diff --git a/src/unit_tests/models/misc/kth_largest_m_tuple.rs b/src/unit_tests/models/misc/kth_largest_m_tuple.rs index e7e905157..5e2906eb4 100644 --- a/src/unit_tests/models/misc/kth_largest_m_tuple.rs +++ b/src/unit_tests/models/misc/kth_largest_m_tuple.rs @@ -31,9 +31,12 @@ fn test_kth_largest_m_tuple_creation() { assert_eq!(p.k(), 14); assert_eq!(p.bound(), 12); assert_eq!(p.num_sets(), 3); - assert_eq!(p.total_tuples(), 18); - assert_eq!(p.dimensions(), Vec::::new()); - assert_eq!(p.num_variables(), 0); + assert_eq!(p.num_elements(), 8); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + Vec::::new() + ); + assert_eq!(p.num_variables().unwrap(), 0); assert_eq!(::NAME, "KthLargestMTuple"); assert_eq!(::variant(), vec![]); } @@ -136,7 +139,7 @@ fn test_kth_largest_m_tuple_all_qualify() { p.evaluate(&solver.solve(&p).unwrap().unwrap()).unwrap(), Or(true) ); - assert_eq!(p.total_tuples(), 1); + assert_eq!(p.num_elements(), 2); } #[test] @@ -169,8 +172,11 @@ fn test_kth_largest_m_tuple_many_singleton_sets_do_not_use_call_stack() { } #[test] -#[should_panic(expected = "total tuple count exceeds usize")] -fn test_kth_largest_m_tuple_total_tuples_overflow_panics() { - let p = KthLargestMTuple::new(vec![vec![1, 2]; usize::BITS as usize], 1, 1); - p.total_tuples(); +fn tuple_product_does_not_restrict_parameters_or_evaluation() { + let p = KthLargestMTuple::new(vec![vec![1, 2]; 64], 1, 1); + assert_eq!(p.num_elements(), 128); + assert_eq!(p.evaluate(&()).unwrap(), Or(true)); + let restored: KthLargestMTuple = + serde_json::from_value(serde_json::to_value(&p).unwrap()).unwrap(); + assert_eq!(p.parameters(), restored.parameters()); } diff --git a/src/unit_tests/models/misc/longest_common_subsequence.rs b/src/unit_tests/models/misc/longest_common_subsequence.rs index 913a300db..cf8c4faeb 100644 --- a/src/unit_tests/models/misc/longest_common_subsequence.rs +++ b/src/unit_tests/models/misc/longest_common_subsequence.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Max; @@ -16,13 +15,14 @@ fn issue_yes_instance() -> LongestCommonSubsequence { vec![1, 0, 1, 0, 1, 0], ], ) + .unwrap() } fn issue_no_instance() -> LongestCommonSubsequence { // All strings have length 3, min = 3, so max_length = 3. // No common subsequence of any positive length exists because // the first string is all 0s and the second is all 1s. - LongestCommonSubsequence::new(2, vec![vec![0, 0, 0], vec![1, 1, 1]]) + LongestCommonSubsequence::new(2, vec![vec![0, 0, 0], vec![1, 1, 1]]).unwrap() } #[test] @@ -35,7 +35,10 @@ fn test_lcs_basic() { assert_eq!(problem.sum_squared_lengths(), 216); assert_eq!(problem.sum_triangular_lengths(), 126); assert_eq!(problem.num_transitions(), 5); - assert_eq!(problem.dimensions(), vec![3; 6]); // alphabet_size + 1 = 3, max_length = 6 + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 6] + ); // alphabet_size + 1 = 3, max_length = 6 assert_eq!( ::NAME, "LongestCommonSubsequence" @@ -102,7 +105,7 @@ fn test_lcs_evaluate_interleaved_padding() { #[test] fn test_lcs_out_of_range_symbol() { - let problem = LongestCommonSubsequence::new(2, vec![vec![0, 1, 0], vec![1, 0, 1]]); + let problem = LongestCommonSubsequence::new(2, vec![vec![0, 1, 0], vec![1, 0, 1]]).unwrap(); // Symbol 3 > alphabet_size (2), but 2 is padding. Symbol 3 is truly out of range. // Actually with alphabet_size=2, valid symbols are 0,1 and padding is 2. Symbol 3 is invalid. // But dims allows 0..2, so symbol 3 wouldn't normally appear. Let's test with a symbol @@ -122,7 +125,7 @@ fn test_lcs_out_of_range_symbol() { #[test] fn test_lcs_bruteforce_finds_optimum() { // Small instance for brute force: alphabet {0,1}, two short strings - let problem = LongestCommonSubsequence::new(2, vec![vec![0, 1, 0], vec![1, 0, 1]]); + let problem = LongestCommonSubsequence::new(2, vec![vec![0, 1, 0], vec![1, 0, 1]]).unwrap(); // max_length = 3, optimal LCS = [0, 1] or [1, 0], length 2 let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap().expect("expected a witness"); @@ -154,36 +157,37 @@ fn test_lcs_serialization() { #[test] fn test_lcs_empty_string_max_length_zero() { // When all strings are empty or any string is empty, max_length = 0 - let problem = LongestCommonSubsequence::new(2, vec![vec![], vec![0, 1]]); + let problem = LongestCommonSubsequence::new(2, vec![vec![], vec![0, 1]]).unwrap(); assert_eq!(problem.max_length(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); // empty config space - // Empty config is the only valid config; LCS length is 0 + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); // empty config space + // Empty config is the only valid config; LCS length is 0 assert_eq!(problem.evaluate(&vec![]).unwrap(), Max(Some(0))); } #[test] fn test_lcs_all_empty_strings() { - let problem = LongestCommonSubsequence::new(2, vec![vec![], vec![]]); + let problem = LongestCommonSubsequence::new(2, vec![vec![], vec![]]).unwrap(); assert_eq!(problem.max_length(), 0); assert_eq!(problem.evaluate(&vec![]).unwrap(), Max(Some(0))); } #[test] -#[should_panic(expected = "alphabet_size must be > 0 when any input string is non-empty")] -fn test_lcs_zero_alphabet_with_nonempty_strings_panics() { - LongestCommonSubsequence::new(0, vec![vec![0]]); +fn test_lcs_zero_alphabet_with_nonempty_strings_is_rejected() { + assert!(LongestCommonSubsequence::new(0, vec![vec![0]]).is_err()); } #[test] -#[should_panic(expected = "input symbols must be less than alphabet_size")] -fn test_lcs_symbol_out_of_range_panics() { - LongestCommonSubsequence::new(2, vec![vec![0, 2]]); +fn test_lcs_symbol_out_of_range_is_rejected() { + assert!(LongestCommonSubsequence::new(2, vec![vec![0, 2]]).is_err()); } #[test] fn test_lcs_full_length_witness() { // When the LCS equals the shortest string length - let problem = LongestCommonSubsequence::new(2, vec![vec![0, 1], vec![0, 1, 0]]); + let problem = LongestCommonSubsequence::new(2, vec![vec![0, 1], vec![0, 1, 0]]).unwrap(); // max_length = 2, optimal LCS = [0, 1], length 2 assert_eq!(problem.max_length(), 2); assert_eq!( @@ -212,11 +216,14 @@ fn test_lcs_create_spec_derives_internal_fields() { } #[test] -fn test_lcs_create_spec_rejects_all_empty_strings() { +fn test_lcs_create_spec_accepts_empty_strings_like_constructor() { let result = LongestCommonSubsequence::try_from(LongestCommonSubsequenceCreateSpec { alphabet_size: Some(2), strings: vec![vec![], vec![]], }); - assert!(result.is_err()); + assert_eq!( + result.unwrap().strings(), + &[Vec::::new(), Vec::new()] + ); } diff --git a/src/unit_tests/models/misc/maximum_likelihood_ranking.rs b/src/unit_tests/models/misc/maximum_likelihood_ranking.rs index 8d06afd26..1aa0e8f77 100644 --- a/src/unit_tests/models/misc/maximum_likelihood_ranking.rs +++ b/src/unit_tests/models/misc/maximum_likelihood_ranking.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -11,11 +10,14 @@ fn test_maximum_likelihood_ranking_creation() { vec![2, 1, 0, 4], vec![0, 2, 1, 0], ]; - let problem = MaximumLikelihoodRanking::new(matrix.clone()); + let problem = MaximumLikelihoodRanking::new(matrix.clone()).unwrap(); assert_eq!(problem.num_items(), 4); assert_eq!(problem.matrix(), &matrix); assert_eq!(problem.comparison_count(), 5); - assert_eq!(problem.dimensions(), vec![4; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 4] + ); assert_eq!( ::NAME, "MaximumLikelihoodRanking" @@ -31,7 +33,7 @@ fn test_maximum_likelihood_ranking_evaluate_optimal() { vec![2, 1, 0, 4], vec![0, 2, 1, 0], ]; - let problem = MaximumLikelihoodRanking::new(matrix); + let problem = MaximumLikelihoodRanking::new(matrix).unwrap(); // Identity ranking: config[i] = i (item i is at position i) // Disagreement pairs where config[a] > config[b]: // (1,0): matrix[1][0] = 1 @@ -52,7 +54,7 @@ fn test_maximum_likelihood_ranking_evaluate_non_permutation() { vec![2, 1, 0, 4], vec![0, 2, 1, 0], ]; - let problem = MaximumLikelihoodRanking::new(matrix); + let problem = MaximumLikelihoodRanking::new(matrix).unwrap(); // Duplicate rank assert_eq!(problem.evaluate(&vec![0, 0, 2, 3]).unwrap(), Min(None)); // Rank out of range @@ -79,7 +81,7 @@ fn test_maximum_likelihood_ranking_evaluate_suboptimal() { vec![2, 1, 0, 4], vec![0, 2, 1, 0], ]; - let problem = MaximumLikelihoodRanking::new(matrix); + let problem = MaximumLikelihoodRanking::new(matrix).unwrap(); // Reversed ranking: config = [3, 2, 1, 0] // (item 0 at pos 3, item 1 at pos 2, item 2 at pos 1, item 3 at pos 0) // Pairs where config[a] > config[b]: @@ -101,7 +103,7 @@ fn test_maximum_likelihood_ranking_solver() { vec![2, 1, 0, 4], vec![0, 2, 1, 0], ]; - let problem = MaximumLikelihoodRanking::new(matrix); + let problem = MaximumLikelihoodRanking::new(matrix).unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -119,7 +121,7 @@ fn test_maximum_likelihood_ranking_serialization() { vec![2, 1, 0, 4], vec![0, 2, 1, 0], ]; - let problem = MaximumLikelihoodRanking::new(matrix.clone()); + let problem = MaximumLikelihoodRanking::new(matrix.clone()).unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: MaximumLikelihoodRanking = serde_json::from_value(json).unwrap(); assert_eq!(restored.matrix(), &matrix); @@ -130,7 +132,7 @@ fn test_maximum_likelihood_ranking_serialization() { fn test_maximum_likelihood_ranking_two_items() { // 2 items: a_01 = 3, a_10 = 2 let matrix = vec![vec![0, 3], vec![2, 0]]; - let problem = MaximumLikelihoodRanking::new(matrix); + let problem = MaximumLikelihoodRanking::new(matrix).unwrap(); // config [0,1]: item 0 at pos 0, item 1 at pos 1 // Only pair where config[a] > config[b]: (1,0) -> matrix[1][0] = 2 assert_eq!(problem.evaluate(&vec![0, 1]).unwrap(), Min(Some(2))); @@ -145,23 +147,24 @@ fn test_maximum_likelihood_ranking_two_items() { #[test] fn test_maximum_likelihood_ranking_single_item() { - let problem = MaximumLikelihoodRanking::new(vec![vec![0]]); + let problem = MaximumLikelihoodRanking::new(vec![vec![0]]).unwrap(); assert_eq!(problem.num_items(), 1); assert_eq!(problem.comparison_count(), 0); - assert_eq!(problem.dimensions(), vec![1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1] + ); assert_eq!(problem.evaluate(&vec![0]).unwrap(), Min(Some(0))); } #[test] -#[should_panic(expected = "matrix must be square")] -fn test_maximum_likelihood_ranking_non_square_panics() { - MaximumLikelihoodRanking::new(vec![vec![0, 1], vec![2, 0], vec![1, 2]]); +fn test_maximum_likelihood_ranking_non_square_rejects() { + assert!(MaximumLikelihoodRanking::new(vec![vec![0, 1], vec![2, 0], vec![1, 2]]).is_err()); } #[test] -#[should_panic(expected = "diagonal entries must be zero")] -fn test_maximum_likelihood_ranking_nonzero_diagonal_panics() { - MaximumLikelihoodRanking::new(vec![vec![1, 2], vec![3, 0]]); +fn test_maximum_likelihood_ranking_nonzero_diagonal_rejects() { + assert!(MaximumLikelihoodRanking::new(vec![vec![1, 2], vec![3, 0]]).is_err()); } #[test] @@ -169,7 +172,7 @@ fn test_maximum_likelihood_ranking_skew_symmetric() { // c = 0: skew-symmetric matrix (a_ij = -a_ji) // Encodes a directed 3-cycle: 0->1, 1->2, 2->0 let matrix = vec![vec![0, 1, -1], vec![-1, 0, 1], vec![1, -1, 0]]; - let problem = MaximumLikelihoodRanking::new(matrix); + let problem = MaximumLikelihoodRanking::new(matrix).unwrap(); assert_eq!(problem.comparison_count(), 0); // Ranking [0,1,2]: 1 backward arc (2->0, cost +1), 2 forward arcs (cost -1 each) // Total = 1 + (-1) + (-1) = -1 = 2*FAS - |A| = 2*1 - 3 @@ -181,9 +184,10 @@ fn test_maximum_likelihood_ranking_skew_symmetric() { } #[test] -#[should_panic(expected = "all off-diagonal pairs must have the same comparison count")] -fn test_maximum_likelihood_ranking_inconsistent_pair_sum_panics() { - MaximumLikelihoodRanking::new(vec![vec![0, 4, 3], vec![1, 0, 4], vec![1, 2, 0]]); +fn test_maximum_likelihood_ranking_inconsistent_pair_sum_rejects() { + assert!( + MaximumLikelihoodRanking::new(vec![vec![0, 4, 3], vec![1, 0, 4], vec![1, 2, 0]]).is_err() + ); } #[cfg(feature = "example-db")] diff --git a/src/unit_tests/models/misc/minimum_axiom_set.rs b/src/unit_tests/models/misc/minimum_axiom_set.rs index f3f746808..5617323a9 100644 --- a/src/unit_tests/models/misc/minimum_axiom_set.rs +++ b/src/unit_tests/models/misc/minimum_axiom_set.rs @@ -19,6 +19,7 @@ fn canonical_instance() -> MinimumAxiomSet { (vec![6, 7], 1), ], ) + .unwrap() } #[test] @@ -28,8 +29,11 @@ fn test_minimum_axiom_set_creation() { assert_eq!(problem.num_true_sentences(), 8); assert_eq!(problem.num_implications(), 8); assert_eq!(problem.true_sentences(), &[0, 1, 2, 3, 4, 5, 6, 7]); - assert_eq!(problem.dimensions(), vec![2; 8]); - assert_eq!(problem.num_variables(), 8); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); + assert_eq!(problem.num_variables().unwrap(), 8); } #[test] @@ -122,10 +126,13 @@ fn test_minimum_axiom_set_serialization() { fn test_minimum_axiom_set_partial_true_sentences() { // Only sentences 0,1,2 are true; implications: ({0}, 1), ({1}, 2) // Optimal: select {0} → closure {0,1,2} = T - let problem = MinimumAxiomSet::new(5, vec![0, 1, 2], vec![(vec![0], 1), (vec![1], 2)]); + let problem = MinimumAxiomSet::new(5, vec![0, 1, 2], vec![(vec![0], 1), (vec![1], 2)]).unwrap(); assert_eq!(problem.num_sentences(), 5); assert_eq!(problem.num_true_sentences(), 3); - assert_eq!(problem.dimensions(), vec![2; 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 3] + ); // Select sentence 0 only let result = problem.evaluate(&vec![true, false, false]).unwrap(); @@ -141,7 +148,7 @@ fn test_minimum_axiom_set_partial_true_sentences() { fn test_minimum_axiom_set_no_implications() { // 3 sentences, all true, no implications // Only way to cover T is to select all of them - let problem = MinimumAxiomSet::new(3, vec![0, 1, 2], vec![]); + let problem = MinimumAxiomSet::new(3, vec![0, 1, 2], vec![]).unwrap(); let result = problem.evaluate(&vec![true, true, true]).unwrap(); assert!(result.is_valid()); assert_eq!(result.unwrap(), 3); diff --git a/src/unit_tests/models/misc/minimum_code_generation_one_register.rs b/src/unit_tests/models/misc/minimum_code_generation_one_register.rs index 78337e23a..f75e17fce 100644 --- a/src/unit_tests/models/misc/minimum_code_generation_one_register.rs +++ b/src/unit_tests/models/misc/minimum_code_generation_one_register.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -19,12 +18,16 @@ fn test_minimum_code_generation_one_register_creation() { (3, 6), ], 3, - ); + ) + .unwrap(); assert_eq!(problem.num_vertices(), 7); assert_eq!(problem.num_edges(), 8); assert_eq!(problem.num_leaves(), 3); assert_eq!(problem.num_internal(), 4); - assert_eq!(problem.dimensions(), vec![4; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 4] + ); assert_eq!( ::NAME, "MinimumCodeGenerationOneRegister" @@ -54,7 +57,8 @@ fn test_minimum_code_generation_one_register_evaluate_optimal() { (3, 6), ], 3, - ); + ) + .unwrap(); let config = vec![3, 2, 1, 0]; assert_eq!(problem.evaluate(&config).unwrap(), Min(Some(8))); assert_eq!(problem.simulate(&config).unwrap(), Some(8)); @@ -78,7 +82,8 @@ fn test_minimum_code_generation_one_register_evaluate_suboptimal() { (3, 6), ], 3, - ); + ) + .unwrap(); // Order: v3 (pos 0), v1 (pos 1), v2 (pos 2), v0 (pos 3) // config: v0->3, v1->1, v2->2, v3->0 let config = vec![3, 1, 2, 0]; @@ -101,7 +106,8 @@ fn test_minimum_code_generation_one_register_invalid_dependency() { (3, 6), ], 3, - ); + ) + .unwrap(); // v0 first (pos 0) — depends on v1,v2 which haven't been computed let config = vec![0, 1, 2, 3]; assert_eq!(problem.evaluate(&config).unwrap(), Min(None)); @@ -122,7 +128,8 @@ fn test_minimum_code_generation_one_register_invalid_permutation() { (3, 6), ], 3, - ); + ) + .unwrap(); // Not a permutation: position 0 used twice assert_eq!(problem.evaluate(&vec![0, 0, 1, 2]).unwrap(), Min(None)); // Wrong length @@ -146,7 +153,8 @@ fn test_minimum_code_generation_one_register_solver() { // Wait — v2 appears as both child and parent? // No: v0 has children v1,v2. v1 has children v2,v3. // Leaves: v2 and v3 have out-degree 0. So num_leaves=2. - let problem = MinimumCodeGenerationOneRegister::new(4, vec![(0, 1), (0, 2), (1, 2), (1, 3)], 2); + let problem = + MinimumCodeGenerationOneRegister::new(4, vec![(0, 1), (0, 2), (1, 2), (1, 3)], 2).unwrap(); let solver = BruteForce::new(); let result_solution = solver.solve(&problem).unwrap().unwrap(); let result = problem.evaluate(&result_solution).unwrap(); @@ -159,7 +167,8 @@ fn test_minimum_code_generation_one_register_solver() { #[test] fn test_minimum_code_generation_one_register_solver_witness() { - let problem = MinimumCodeGenerationOneRegister::new(4, vec![(0, 1), (0, 2), (1, 2), (1, 3)], 2); + let problem = + MinimumCodeGenerationOneRegister::new(4, vec![(0, 1), (0, 2), (1, 2), (1, 3)], 2).unwrap(); let solver = BruteForce::new(); let witness = solver .solve(&problem) @@ -183,7 +192,8 @@ fn test_minimum_code_generation_one_register_serialization() { (3, 6), ], 3, - ); + ) + .unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: MinimumCodeGenerationOneRegister = serde_json::from_value(json).unwrap(); assert_eq!(restored.num_vertices(), problem.num_vertices()); @@ -196,7 +206,7 @@ fn test_minimum_code_generation_one_register_serialization() { fn test_minimum_code_generation_one_register_unary_ops() { // Simple chain: v0 = unary(v1), v1 = unary(v2) // Leaves: {2}, Internal: {0, 1} - let problem = MinimumCodeGenerationOneRegister::new(3, vec![(0, 1), (1, 2)], 1); + let problem = MinimumCodeGenerationOneRegister::new(3, vec![(0, 1), (1, 2)], 1).unwrap(); // Order: v1 first, v0 second. config = [1, 0] let config = vec![1, 0]; // v1: LOAD v2, OP v1 = 2 @@ -222,7 +232,8 @@ fn test_minimum_code_generation_one_register_paper_example() { (3, 6), ], 3, - ); + ) + .unwrap(); // Optimal order: v3, v2, v1, v0 => config = [3, 2, 1, 0] let config = vec![3, 2, 1, 0]; @@ -250,7 +261,8 @@ fn test_minimum_code_generation_one_register_lost_value() { 6, vec![(0, 1), (0, 2), (1, 3), (1, 4), (2, 3), (2, 5)], 3, - ); + ) + .unwrap(); // Order: v1, v2, v0 => config: v0->2, v1->0, v2->1 let config = vec![2, 0, 1]; // v1 computed first, but v1 is needed by v0. diff --git a/src/unit_tests/models/misc/minimum_code_generation_parallel_assignments.rs b/src/unit_tests/models/misc/minimum_code_generation_parallel_assignments.rs index 541ed2e59..0a72f36bb 100644 --- a/src/unit_tests/models/misc/minimum_code_generation_parallel_assignments.rs +++ b/src/unit_tests/models/misc/minimum_code_generation_parallel_assignments.rs @@ -1,16 +1,18 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] fn test_minimum_code_generation_parallel_assignments_creation() { let assignments = vec![(0, vec![1, 2]), (1, vec![0]), (2, vec![3]), (3, vec![1, 2])]; - let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments.clone()); + let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments.clone()).unwrap(); assert_eq!(problem.num_variables(), 4); assert_eq!(problem.num_assignments(), 4); assert_eq!(problem.assignments(), &assignments); - assert_eq!(problem.dimensions(), vec![4; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 4] + ); assert_eq!( ::NAME, "MinimumCodeGenerationParallelAssignments" @@ -24,7 +26,7 @@ fn test_minimum_code_generation_parallel_assignments_creation() { #[test] fn test_minimum_code_generation_parallel_assignments_evaluate_optimal() { let assignments = vec![(0, vec![1, 2]), (1, vec![0]), (2, vec![3]), (3, vec![1, 2])]; - let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments); + let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments).unwrap(); // Config [0, 3, 1, 2]: A_0 at pos 0, A_1 at pos 3, A_2 at pos 1, A_3 at pos 2 // Order: (A_0, A_2, A_3, A_1) // A_0 writes a(0): A_1 reads a and is later (pos 3) -> 1 backward dep @@ -37,7 +39,7 @@ fn test_minimum_code_generation_parallel_assignments_evaluate_optimal() { #[test] fn test_minimum_code_generation_parallel_assignments_evaluate_suboptimal() { let assignments = vec![(0, vec![1, 2]), (1, vec![0]), (2, vec![3]), (3, vec![1, 2])]; - let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments); + let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments).unwrap(); // Config [1, 0, 2, 3]: A_0 at pos 1, A_1 at pos 0, A_2 at pos 2, A_3 at pos 3 // Order: (A_1, A_0, A_2, A_3) // A_1 writes b(1): A_0 reads b (later, pos 1) -> 1; A_3 reads b (later, pos 3) -> 1 @@ -50,7 +52,7 @@ fn test_minimum_code_generation_parallel_assignments_evaluate_suboptimal() { #[test] fn test_minimum_code_generation_parallel_assignments_evaluate_invalid() { let assignments = vec![(0, vec![1, 2]), (1, vec![0]), (2, vec![3]), (3, vec![1, 2])]; - let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments); + let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments).unwrap(); // Duplicate position assert_eq!(problem.evaluate(&vec![0, 0, 1, 2]).unwrap(), Min(None)); // Out of range @@ -72,7 +74,7 @@ fn test_minimum_code_generation_parallel_assignments_evaluate_invalid() { #[test] fn test_minimum_code_generation_parallel_assignments_solver() { let assignments = vec![(0, vec![1, 2]), (1, vec![0]), (2, vec![3]), (3, vec![1, 2])]; - let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments); + let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments).unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -85,7 +87,7 @@ fn test_minimum_code_generation_parallel_assignments_solver() { #[test] fn test_minimum_code_generation_parallel_assignments_serialization() { let assignments = vec![(0, vec![1, 2]), (1, vec![0]), (2, vec![3]), (3, vec![1, 2])]; - let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments.clone()); + let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments.clone()).unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: MinimumCodeGenerationParallelAssignments = serde_json::from_value(json).unwrap(); assert_eq!(restored.num_variables(), 4); @@ -99,7 +101,7 @@ fn test_minimum_code_generation_parallel_assignments_no_dependencies() { (0, vec![2]), // writes a, reads c (1, vec![3]), // writes b, reads d ]; - let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments); + let problem = MinimumCodeGenerationParallelAssignments::new(4, assignments).unwrap(); // Neither assignment reads the target of the other assert_eq!(problem.evaluate(&vec![0, 1]).unwrap(), Min(Some(0))); assert_eq!(problem.evaluate(&vec![1, 0]).unwrap(), Min(Some(0))); @@ -109,15 +111,13 @@ fn test_minimum_code_generation_parallel_assignments_no_dependencies() { } #[test] -#[should_panic(expected = "target variable")] -fn test_minimum_code_generation_parallel_assignments_invalid_target_panics() { - MinimumCodeGenerationParallelAssignments::new(2, vec![(2, vec![0])]); +fn test_minimum_code_generation_parallel_assignments_invalid_target_rejects() { + assert!(MinimumCodeGenerationParallelAssignments::new(2, vec![(2, vec![0])]).is_err()); } #[test] -#[should_panic(expected = "read variable")] -fn test_minimum_code_generation_parallel_assignments_invalid_read_panics() { - MinimumCodeGenerationParallelAssignments::new(2, vec![(0, vec![3])]); +fn test_minimum_code_generation_parallel_assignments_invalid_read_rejects() { + assert!(MinimumCodeGenerationParallelAssignments::new(2, vec![(0, vec![3])]).is_err()); } #[cfg(feature = "example-db")] diff --git a/src/unit_tests/models/misc/minimum_code_generation_unlimited_registers.rs b/src/unit_tests/models/misc/minimum_code_generation_unlimited_registers.rs index c852009b8..5bb691873 100644 --- a/src/unit_tests/models/misc/minimum_code_generation_unlimited_registers.rs +++ b/src/unit_tests/models/misc/minimum_code_generation_unlimited_registers.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -10,13 +9,17 @@ fn test_minimum_code_generation_unlimited_registers_creation() { 5, vec![(1, 3), (2, 3), (0, 1)], vec![(1, 4), (2, 4), (0, 2)], - ); + ) + .unwrap(); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_leaves(), 2); assert_eq!(problem.num_internal(), 3); assert_eq!(problem.left_arcs(), &[(1, 3), (2, 3), (0, 1)]); assert_eq!(problem.right_arcs(), &[(1, 4), (2, 4), (0, 2)]); - assert_eq!(problem.dimensions(), vec![3; 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 3] + ); assert_eq!( ::NAME, "MinimumCodeGenerationUnlimitedRegisters" @@ -37,7 +40,8 @@ fn test_minimum_code_generation_unlimited_registers_evaluate_optimal() { 5, vec![(1, 3), (2, 3), (0, 1)], vec![(1, 4), (2, 4), (0, 2)], - ); + ) + .unwrap(); let config = vec![2, 0, 1]; assert_eq!(problem.evaluate(&config).unwrap(), Min(Some(4))); assert_eq!(problem.simulate(&config).unwrap(), Some(4)); @@ -70,7 +74,8 @@ fn test_minimum_code_generation_unlimited_registers_evaluate_suboptimal() { 5, vec![(1, 3), (2, 3), (0, 1)], vec![(1, 4), (2, 4), (0, 2)], - ); + ) + .unwrap(); let config = vec![2, 1, 0]; // Step 0: OP v2, left=v3. future uses of v3 after decrement: left_uses=1 (from v1), right_uses=0. // Still needed -> LOAD v3. instructions = 2 (1 LOAD + 1 OP). @@ -88,7 +93,8 @@ fn test_minimum_code_generation_unlimited_registers_dependency_violation() { 5, vec![(1, 3), (2, 3), (0, 1)], vec![(1, 4), (2, 4), (0, 2)], - ); + ) + .unwrap(); // v0 first (pos 0) — depends on v1,v2 which haven't been computed let config = vec![0, 1, 2]; assert_eq!(problem.evaluate(&config).unwrap(), Min(None)); @@ -100,7 +106,8 @@ fn test_minimum_code_generation_unlimited_registers_invalid_permutation() { 5, vec![(1, 3), (2, 3), (0, 1)], vec![(1, 4), (2, 4), (0, 2)], - ); + ) + .unwrap(); // Not a permutation: position 0 used twice assert_eq!(problem.evaluate(&vec![0, 0, 1]).unwrap(), Min(None)); // Wrong length @@ -122,7 +129,8 @@ fn test_minimum_code_generation_unlimited_registers_solver() { 5, vec![(1, 3), (2, 3), (0, 1)], vec![(1, 4), (2, 4), (0, 2)], - ); + ) + .unwrap(); let solver = BruteForce::new(); let result_solution = solver.solve(&problem).unwrap().unwrap(); let result = problem.evaluate(&result_solution).unwrap(); @@ -135,7 +143,8 @@ fn test_minimum_code_generation_unlimited_registers_solver_witness() { 5, vec![(1, 3), (2, 3), (0, 1)], vec![(1, 4), (2, 4), (0, 2)], - ); + ) + .unwrap(); let solver = BruteForce::new(); let witness = solver .solve(&problem) @@ -150,7 +159,8 @@ fn test_minimum_code_generation_unlimited_registers_serialization() { 5, vec![(1, 3), (2, 3), (0, 1)], vec![(1, 4), (2, 4), (0, 2)], - ); + ) + .unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: MinimumCodeGenerationUnlimitedRegisters = serde_json::from_value(json).unwrap(); assert_eq!(restored.num_vertices(), problem.num_vertices()); @@ -164,7 +174,8 @@ fn test_minimum_code_generation_unlimited_registers_unary_ops() { // Simple chain: v0 = unary(v1), v1 = unary(v2) // Leaves: {2}, Internal: {0, 1} // Unary ops only have left arcs - let problem = MinimumCodeGenerationUnlimitedRegisters::new(3, vec![(0, 1), (1, 2)], vec![]); + let problem = + MinimumCodeGenerationUnlimitedRegisters::new(3, vec![(0, 1), (1, 2)], vec![]).unwrap(); // Order: v1 first, v0 second. config = [1, 0] let config = vec![1, 0]; // v1: left=v2, no future uses of v2 -> no LOAD. OP v1 = 1. @@ -178,7 +189,8 @@ fn test_minimum_code_generation_unlimited_registers_unary_ops() { fn test_minimum_code_generation_unlimited_registers_no_copy_needed() { // v0 = op(v1, v2), v1 and v2 are leaves // No shared operands, so no copies needed - let problem = MinimumCodeGenerationUnlimitedRegisters::new(3, vec![(0, 1)], vec![(0, 2)]); + let problem = + MinimumCodeGenerationUnlimitedRegisters::new(3, vec![(0, 1)], vec![(0, 2)]).unwrap(); // Only one internal vertex v0, config = [0] let config = vec![0]; // OP v0: left=v1, right=v2. No future uses of v1. No LOAD. 1 OP. @@ -192,7 +204,8 @@ fn test_minimum_code_generation_unlimited_registers_paper_example() { 5, vec![(1, 3), (2, 3), (0, 1)], vec![(1, 4), (2, 4), (0, 2)], - ); + ) + .unwrap(); // Optimal order: v1, v2, v0 => config = [2, 0, 1] let config = vec![2, 0, 1]; diff --git a/src/unit_tests/models/misc/minimum_decision_tree.rs b/src/unit_tests/models/misc/minimum_decision_tree.rs index 561066d05..28b43865a 100644 --- a/src/unit_tests/models/misc/minimum_decision_tree.rs +++ b/src/unit_tests/models/misc/minimum_decision_tree.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_indistinguishable_objects() { @@ -26,6 +25,7 @@ fn issue_instance() -> MinimumDecisionTree { 4, 3, ) + .unwrap() } #[test] @@ -33,8 +33,16 @@ fn test_minimum_decision_tree_creation() { let problem = issue_instance(); assert_eq!(problem.num_objects(), 4); assert_eq!(problem.num_tests(), 3); - assert_eq!(problem.dimensions().len(), 7); // 2^(4-1) - 1 = 7 - assert_eq!(problem.dimensions(), vec![4; 7]); // 3 tests + 1 sentinel = 4 choices + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 7 + ); // 2^(4-1) - 1 = 7 + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 7] + ); // 3 tests + 1 sentinel = 4 choices } #[test] @@ -110,22 +118,26 @@ fn test_minimum_decision_tree_two_objects() { vec![vec![false, true]], // T0 distinguishes o0 (false) from o1 (true) 2, 1, - ); - assert_eq!(problem.dimensions().len(), 1); // 2^(2-1) - 1 = 1 slot - // Test at root, both objects go to leaves at depth 1 + ) + .unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 1 + ); // 2^(2-1) - 1 = 1 slot + // Test at root, both objects go to leaves at depth 1 assert_eq!(problem.evaluate(&vec![0]).unwrap(), Min(Some(2))); // depth 1 + depth 1 assert_eq!(problem.evaluate(&vec![1]).unwrap(), Min(None)); // sentinel=1 is leaf at root, both objects at same leaf } #[test] -#[should_panic(expected = "Need at least 2 objects")] fn test_minimum_decision_tree_too_few_objects() { - MinimumDecisionTree::new(vec![vec![true]], 1, 1); + assert!(MinimumDecisionTree::new(vec![vec![true]], 1, 1).is_err()); } #[test] -#[should_panic(expected = "not distinguished")] fn test_minimum_decision_tree_indistinguishable() { // Two objects with identical test results - MinimumDecisionTree::new(vec![vec![true, true]], 2, 1); + assert!(MinimumDecisionTree::new(vec![vec![true, true]], 2, 1).is_err()); } diff --git a/src/unit_tests/models/misc/minimum_discrete_planar_inverse_kinematics.rs b/src/unit_tests/models/misc/minimum_discrete_planar_inverse_kinematics.rs index 6c82a88b4..b583a0665 100644 --- a/src/unit_tests/models/misc/minimum_discrete_planar_inverse_kinematics.rs +++ b/src/unit_tests/models/misc/minimum_discrete_planar_inverse_kinematics.rs @@ -25,8 +25,11 @@ fn test_minimum_discrete_planar_inverse_kinematics_creation() { assert_eq!(problem.target_point(), (2.0, 1.0)); assert_eq!(problem.orientation_samples().len(), 2); assert_eq!(problem.allowed_pairs().len(), 1); - assert_eq!(problem.dimensions(), vec![2, 2]); - assert_eq!(problem.num_variables(), 2); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2] + ); + assert_eq!(problem.num_variables().unwrap(), 2); assert_eq!(problem.num_orientation_samples(), 4); } @@ -116,7 +119,10 @@ fn test_minimum_discrete_planar_inverse_kinematics_serialization() { problem.orientation_samples() ); assert_eq!(restored.allowed_pairs(), problem.allowed_pairs()); - assert_eq!(restored.dimensions(), problem.dimensions()); + assert_eq!( + crate::solvers::cartesian_dimensions(&restored).unwrap(), + crate::solvers::cartesian_dimensions(&problem).unwrap() + ); } #[test] diff --git a/src/unit_tests/models/misc/minimum_disjunctive_normal_form.rs b/src/unit_tests/models/misc/minimum_disjunctive_normal_form.rs index 75c7388ee..db6e1f0ac 100644 --- a/src/unit_tests/models/misc/minimum_disjunctive_normal_form.rs +++ b/src/unit_tests/models/misc/minimum_disjunctive_normal_form.rs @@ -1,12 +1,12 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; fn issue_instance() -> MinimumDisjunctiveNormalForm { // f(x1,x2,x3) = 1 when exactly 1 or 2 variables are true MinimumDisjunctiveNormalForm::new(3, vec![false, true, true, true, true, true, true, false]) + .unwrap() } #[test] @@ -15,7 +15,10 @@ fn test_minimum_dnf_creation() { assert_eq!(problem.num_variables(), 3); assert_eq!(problem.minterms().len(), 6); assert_eq!(problem.num_prime_implicants(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); } #[test] @@ -91,7 +94,7 @@ fn test_minimum_dnf_serialization() { #[test] fn test_minimum_dnf_two_variables() { // f(x1,x2) = x1 XOR x2 = {01, 10} - let problem = MinimumDisjunctiveNormalForm::new(2, vec![false, true, true, false]); + let problem = MinimumDisjunctiveNormalForm::new(2, vec![false, true, true, false]).unwrap(); assert_eq!(problem.minterms(), &[1, 2]); // Prime implicants: ¬x1∧x2 covers {01}, x1∧¬x2 covers {10} assert_eq!(problem.num_prime_implicants(), 2); @@ -105,7 +108,7 @@ fn test_minimum_dnf_two_variables() { #[test] fn test_minimum_dnf_single_minterm() { // f(x1,x2) = x1 AND x2 = {11} - let problem = MinimumDisjunctiveNormalForm::new(2, vec![false, false, false, true]); + let problem = MinimumDisjunctiveNormalForm::new(2, vec![false, false, false, true]).unwrap(); assert_eq!(problem.minterms(), &[3]); assert_eq!(problem.num_prime_implicants(), 1); // x1∧x2 let solver = BruteForce::new(); @@ -140,7 +143,17 @@ fn test_minimum_dnf_wrong_config_length() { } #[test] -#[should_panic(expected = "at least one minterm")] fn test_minimum_dnf_all_false() { - MinimumDisjunctiveNormalForm::new(2, vec![false, false, false, false]); + assert!(MinimumDisjunctiveNormalForm::new(2, vec![false, false, false, false]).is_err()); +} + +#[test] +fn deserialize_rebuilds_prime_implicants() { + let model: MinimumDisjunctiveNormalForm = serde_json::from_value(serde_json::json!({ + "num_variables": 2, "truth_table": [false, true, true, false], + "prime_implicants": [], "minterms": [99] + })) + .unwrap(); + assert_eq!(model.minterms(), &[1, 2]); + assert_eq!(model.num_prime_implicants(), 2); } diff --git a/src/unit_tests/models/misc/minimum_external_macro_data_compression.rs b/src/unit_tests/models/misc/minimum_external_macro_data_compression.rs index 13b50275b..07b6b5944 100644 --- a/src/unit_tests/models/misc/minimum_external_macro_data_compression.rs +++ b/src/unit_tests/models/misc/minimum_external_macro_data_compression.rs @@ -1,12 +1,11 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; #[test] fn test_minimum_external_macro_data_compression_creation() { - let problem = MinimumExternalMacroDataCompression::new(3, vec![0, 1, 2, 0, 1, 2], 2); + let problem = MinimumExternalMacroDataCompression::new(3, vec![0, 1, 2, 0, 1, 2], 2).unwrap(); assert_eq!(problem.alphabet_size(), 3); assert_eq!(problem.string_length(), 6); assert_eq!(problem.pointer_cost(), 2); @@ -20,7 +19,7 @@ fn test_minimum_external_macro_data_compression_creation() { vec![] ); // dims: 6 D-slots (domain 4) + 6 C-slots (domain 4 + 6*7/2 = 25) - let dims = problem.dimensions(); + let dims = crate::solvers::cartesian_dimensions(&problem).unwrap(); assert_eq!(dims.len(), 12); assert_eq!(dims[0], 4); // alphabet_size + 1 assert_eq!(dims[6], 25); // alphabet_size + 1 + 6*7/2 @@ -29,7 +28,7 @@ fn test_minimum_external_macro_data_compression_creation() { #[test] fn test_minimum_external_macro_data_compression_evaluate_uncompressed() { // alphabet {a, b}, s = "ab", h = 2 - let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); // Uncompressed: D = "" (empty, empty), C = "ab" // D-slots: [2, 2] (both empty) // C-slots: [0, 1] (literal a, literal b) @@ -40,7 +39,7 @@ fn test_minimum_external_macro_data_compression_evaluate_uncompressed() { #[test] fn test_minimum_external_macro_data_compression_evaluate_with_pointer() { // alphabet {a, b}, s = "abab", h = 2 - let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2); + let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2).unwrap(); // D = "ab" (len 2), C = "ptr(0,2) ptr(0,2)" // D-slots: [0, 1, 2, 2] (a, b, empty, empty) // C-slots: pointer (0,2) = index 1 in pointer enumeration: @@ -58,14 +57,14 @@ fn test_minimum_external_macro_data_compression_evaluate_with_pointer() { #[test] fn test_minimum_external_macro_data_compression_evaluate_invalid_decode() { // alphabet {a, b}, s = "ab", h = 2 - let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); // C = "ba" doesn't match s = "ab" assert_eq!(problem.evaluate(&vec![2, 2, 1, 0]).unwrap(), Min(None)); } #[test] fn test_minimum_external_macro_data_compression_evaluate_wrong_length() { - let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); assert!(matches!( problem.evaluate(&vec![0, 1, 0]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -79,7 +78,7 @@ fn test_minimum_external_macro_data_compression_evaluate_wrong_length() { #[test] fn test_minimum_external_macro_data_compression_evaluate_interleaved_empty() { // D-slots have interleaved empty - let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); // D-slots: [2, 0] (empty then non-empty -> invalid) assert_eq!(problem.evaluate(&vec![2, 0, 0, 1]).unwrap(), Min(None)); } @@ -87,7 +86,7 @@ fn test_minimum_external_macro_data_compression_evaluate_interleaved_empty() { #[test] fn test_minimum_external_macro_data_compression_evaluate_pointer_out_of_range() { // alphabet {a, b}, s = "ab", h = 2 - let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); // D = "a" (len 1), C = "ptr(0,2)" which references D[0..2] but D only has 1 element // ptr(0,2) index = 1, encoded as 2+1+1 = 4 assert_eq!(problem.evaluate(&vec![0, 2, 4, 2]).unwrap(), Min(None)); @@ -95,8 +94,11 @@ fn test_minimum_external_macro_data_compression_evaluate_pointer_out_of_range() #[test] fn test_minimum_external_macro_data_compression_empty_string() { - let problem = MinimumExternalMacroDataCompression::new(2, vec![], 2); - assert_eq!(problem.dimensions(), Vec::::new()); + let problem = MinimumExternalMacroDataCompression::new(2, vec![], 2).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } @@ -104,7 +106,7 @@ fn test_minimum_external_macro_data_compression_empty_string() { fn test_minimum_external_macro_data_compression_brute_force() { // alphabet {a, b}, s = "ab", h = 2 // Search space: 3^2 * 6^2 = 324 (feasible for brute force) - let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); let solver = BruteForce::new(); let witness = solver .solve(&problem) @@ -118,7 +120,7 @@ fn test_minimum_external_macro_data_compression_brute_force() { #[test] fn test_minimum_external_macro_data_compression_solve_aggregate() { - let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); let solver = BruteForce::new(); let val_solution = solver.solve(&problem).unwrap().unwrap(); let val = problem.evaluate(&val_solution).unwrap(); @@ -127,7 +129,7 @@ fn test_minimum_external_macro_data_compression_solve_aggregate() { #[test] fn test_minimum_external_macro_data_compression_serialization() { - let problem = MinimumExternalMacroDataCompression::new(3, vec![0, 1, 2], 2); + let problem = MinimumExternalMacroDataCompression::new(3, vec![0, 1, 2], 2).unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: MinimumExternalMacroDataCompression = serde_json::from_value(json).unwrap(); assert_eq!(restored.alphabet_size(), problem.alphabet_size()); @@ -154,7 +156,8 @@ fn test_minimum_external_macro_data_compression_paper_example() { 6, vec![0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5], 2, - ); + ) + .unwrap(); assert_eq!(problem.string_length(), 18); // Construct the optimal config manually: @@ -178,7 +181,7 @@ fn test_minimum_external_macro_data_compression_paper_example() { fn test_minimum_external_macro_data_compression_find_all_witnesses() { // alphabet {a}, s = "a", h = 2 // 2*1 = 2 variables. D-domain = 2, C-domain = 2 + 1 = 3. Total = 2*3 = 6 - let problem = MinimumExternalMacroDataCompression::new(1, vec![0], 2); + let problem = MinimumExternalMacroDataCompression::new(1, vec![0], 2).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); // There should be at least one witness: uncompressed [1, 0] (D=empty, C=a) diff --git a/src/unit_tests/models/misc/minimum_fault_detection_test_set.rs b/src/unit_tests/models/misc/minimum_fault_detection_test_set.rs index 50b725e11..17135f5fd 100644 --- a/src/unit_tests/models/misc/minimum_fault_detection_test_set.rs +++ b/src/unit_tests/models/misc/minimum_fault_detection_test_set.rs @@ -22,6 +22,7 @@ fn issue_problem() -> MinimumFaultDetectionTestSet { vec![0, 1], vec![5, 6], ) + .unwrap() } #[test] @@ -35,8 +36,11 @@ fn test_minimum_fault_detection_test_set_creation() { assert_eq!(problem.num_inputs(), 2); assert_eq!(problem.num_outputs(), 2); // 2 inputs * 2 outputs = 4 pairs - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!( ::NAME, "MinimumFaultDetectionTestSet" @@ -107,7 +111,7 @@ fn test_minimum_fault_detection_test_set_evaluate_no_selection() { #[test] fn test_minimum_fault_detection_test_set_counts_only_internal_vertices() { - let problem = MinimumFaultDetectionTestSet::new(2, vec![(0, 1)], vec![0], vec![1]); + let problem = MinimumFaultDetectionTestSet::new(2, vec![(0, 1)], vec![0], vec![1]).unwrap(); // With only an input and an output, there are no internal vertices to cover. assert_eq!(problem.evaluate(&vec![vec![false]]).unwrap(), Min(Some(0))); @@ -193,3 +197,15 @@ fn test_minimum_fault_detection_test_set_paper_example() { vec![vec![true, false], vec![false, true]] ); } + +#[test] +fn deserialize_rejects_invalid_vertices_before_building_coverage() { + for (arcs, inputs) in [(vec![(0, 2)], vec![0]), (vec![(0, 1)], vec![2])] { + assert!( + serde_json::from_value::(serde_json::json!({ + "num_vertices": 2, "arcs": arcs, "inputs": inputs, "outputs": [1] + })) + .is_err() + ); + } +} diff --git a/src/unit_tests/models/misc/minimum_internal_macro_data_compression.rs b/src/unit_tests/models/misc/minimum_internal_macro_data_compression.rs index 82be1e6e6..0b8078ad5 100644 --- a/src/unit_tests/models/misc/minimum_internal_macro_data_compression.rs +++ b/src/unit_tests/models/misc/minimum_internal_macro_data_compression.rs @@ -1,12 +1,12 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; #[test] fn test_minimum_internal_macro_data_compression_creation() { - let problem = MinimumInternalMacroDataCompression::new(3, vec![0, 1, 2, 0, 1, 2, 0, 1, 2], 2); + let problem = + MinimumInternalMacroDataCompression::new(3, vec![0, 1, 2, 0, 1, 2, 0, 1, 2], 2).unwrap(); assert_eq!(problem.alphabet_size(), 3); assert_eq!(problem.string_len(), 9); assert_eq!(problem.pointer_cost(), 2); @@ -20,7 +20,7 @@ fn test_minimum_internal_macro_data_compression_creation() { vec![] ); // dims: 9 slots, domain = 3 + 9 + 1 = 13 - let dims = problem.dimensions(); + let dims = crate::solvers::cartesian_dimensions(&problem).unwrap(); assert_eq!(dims.len(), 9); assert!(dims.iter().all(|&d| d == 13)); } @@ -28,7 +28,7 @@ fn test_minimum_internal_macro_data_compression_creation() { #[test] fn test_minimum_internal_macro_data_compression_evaluate_uncompressed() { // alphabet {a, b}, s = "ab", h = 2 - let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); // Uncompressed: C = [a, b] = [0, 1] // active_len = 2, pointers = 0 // cost = 2 + 0 = 2 @@ -38,7 +38,7 @@ fn test_minimum_internal_macro_data_compression_evaluate_uncompressed() { #[test] fn test_minimum_internal_macro_data_compression_evaluate_with_pointer() { // alphabet {a, b}, s = "abab", h = 2 - let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2); + let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2).unwrap(); // C = [a, b, ptr(0), EOS] = [0, 1, 3, 2] // ptr(0) at position 2: refs decoded[0] = 'a', greedy match: 'a','b' = "ab" // decoded = "abab" = s @@ -50,14 +50,14 @@ fn test_minimum_internal_macro_data_compression_evaluate_with_pointer() { #[test] fn test_minimum_internal_macro_data_compression_evaluate_invalid_decode() { // alphabet {a, b}, s = "ab", h = 2 - let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); // C = [b, a] decodes to "ba" != "ab" assert_eq!(problem.evaluate(&vec![1, 0]).unwrap(), Min(None)); } #[test] fn test_minimum_internal_macro_data_compression_evaluate_wrong_length() { - let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); assert!(matches!( problem.evaluate(&vec![0]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -71,7 +71,7 @@ fn test_minimum_internal_macro_data_compression_evaluate_wrong_length() { #[test] fn test_minimum_internal_macro_data_compression_evaluate_interleaved_eos() { // EOS then non-EOS is invalid - let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); // config = [EOS, a] = [2, 0] assert_eq!(problem.evaluate(&vec![2, 0]).unwrap(), Min(None)); } @@ -79,7 +79,7 @@ fn test_minimum_internal_macro_data_compression_evaluate_interleaved_eos() { #[test] fn test_minimum_internal_macro_data_compression_evaluate_pointer_forward_ref() { // alphabet {a, b}, s = "ab", h = 2 - let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); // C = [ptr(0)] -> pointer at first position references decoded[0], but nothing decoded yet // ptr(C[0]) encoded as 3 (alphabet_size + 1 + 0 = 2+1+0 = 3) assert_eq!(problem.evaluate(&vec![3, 2]).unwrap(), Min(None)); @@ -87,8 +87,11 @@ fn test_minimum_internal_macro_data_compression_evaluate_pointer_forward_ref() { #[test] fn test_minimum_internal_macro_data_compression_empty_string() { - let problem = MinimumInternalMacroDataCompression::new(2, vec![], 2); - assert_eq!(problem.dimensions(), Vec::::new()); + let problem = MinimumInternalMacroDataCompression::new(2, vec![], 2).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } @@ -96,7 +99,7 @@ fn test_minimum_internal_macro_data_compression_empty_string() { fn test_minimum_internal_macro_data_compression_brute_force_simple() { // alphabet {a, b}, s = "ab", h = 2 // Only valid compression is uncompressed [0, 1], cost = 2 - let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); let solver = BruteForce::new(); let witness = solver .solve(&problem) @@ -110,7 +113,7 @@ fn test_minimum_internal_macro_data_compression_brute_force_simple() { fn test_minimum_internal_macro_data_compression_brute_force_repeated() { // alphabet {a, b}, s = "abab", h = 2 // domain = 2+4+1 = 7, 7^4 = 2401 configs (feasible) - let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2); + let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2).unwrap(); let solver = BruteForce::new(); let witness = solver .solve(&problem) @@ -125,7 +128,7 @@ fn test_minimum_internal_macro_data_compression_brute_force_repeated() { #[test] fn test_minimum_internal_macro_data_compression_solve_aggregate() { - let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); let solver = BruteForce::new(); let val_solution = solver.solve(&problem).unwrap().unwrap(); let val = problem.evaluate(&val_solution).unwrap(); @@ -134,7 +137,7 @@ fn test_minimum_internal_macro_data_compression_solve_aggregate() { #[test] fn test_minimum_internal_macro_data_compression_serialization() { - let problem = MinimumInternalMacroDataCompression::new(3, vec![0, 1, 2], 2); + let problem = MinimumInternalMacroDataCompression::new(3, vec![0, 1, 2], 2).unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: MinimumInternalMacroDataCompression = serde_json::from_value(json).unwrap(); assert_eq!(restored.alphabet_size(), problem.alphabet_size()); @@ -147,7 +150,8 @@ fn test_minimum_internal_macro_data_compression_paper_example() { // Issue example: alphabet {a,b,c} (3), s="abcabcabc" (9), h=2 // Optimal: C = [a, b, c, ptr(0), ptr(0), EOS, EOS, EOS, EOS] // active_len=5, pointers=2, cost = 5 + 1*2 = 7 - let problem = MinimumInternalMacroDataCompression::new(3, vec![0, 1, 2, 0, 1, 2, 0, 1, 2], 2); + let problem = + MinimumInternalMacroDataCompression::new(3, vec![0, 1, 2, 0, 1, 2, 0, 1, 2], 2).unwrap(); let config = vec![0, 1, 2, 4, 4, 3, 3, 3, 3]; // ptr(C[0]) = alphabet_size + 1 + 0 = 3 + 1 + 0 = 4 let val = problem.evaluate(&config).unwrap(); @@ -158,7 +162,7 @@ fn test_minimum_internal_macro_data_compression_paper_example() { fn test_minimum_internal_macro_data_compression_find_all_witnesses() { // alphabet {a}, s = "a", h = 2 // domain = 1+1+1 = 3, 3^1 = 3 configs - let problem = MinimumInternalMacroDataCompression::new(1, vec![0], 2); + let problem = MinimumInternalMacroDataCompression::new(1, vec![0], 2).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); // Only valid: [0] (literal 'a'), cost = 1 @@ -176,7 +180,7 @@ fn test_minimum_internal_macro_data_compression_pointer_doubling() { // - pos 2: ptr(0), copy decoded[0..2]="aa" (2 chars), decoded=[0,0,0,0] // decoded = "aaaa" = s // active_len = 3, pointers = 2, cost = 3 + 0*2 = 3 - let problem = MinimumInternalMacroDataCompression::new(1, vec![0, 0, 0, 0], 1); + let problem = MinimumInternalMacroDataCompression::new(1, vec![0, 0, 0, 0], 1).unwrap(); let config = vec![0, 2, 2, 1]; // a, ptr(0), ptr(0), EOS let val = problem.evaluate(&config).unwrap(); assert_eq!(val, Min(Some(3))); diff --git a/src/unit_tests/models/misc/minimum_register_sufficiency_for_loops.rs b/src/unit_tests/models/misc/minimum_register_sufficiency_for_loops.rs index 1c07a636d..fd129fc88 100644 --- a/src/unit_tests/models/misc/minimum_register_sufficiency_for_loops.rs +++ b/src/unit_tests/models/misc/minimum_register_sufficiency_for_loops.rs @@ -1,22 +1,24 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; #[test] fn test_creation() { - let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3), (4, 3)]); + let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3), (4, 3)]).unwrap(); assert_eq!(problem.loop_length(), 6); assert_eq!(problem.num_variables(), 3); assert_eq!(problem.variables(), &[(0, 3), (2, 3), (4, 3)]); - assert_eq!(problem.dimensions(), vec![3, 3, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3, 3] + ); } #[test] fn test_evaluate_optimal() { // K3 graph: all 3 vars conflict, need 3 registers - let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3), (4, 3)]); + let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3), (4, 3)]).unwrap(); let result = problem.evaluate(&vec![0, 1, 2]).unwrap(); assert_eq!(result, Min(Some(3))); } @@ -24,7 +26,7 @@ fn test_evaluate_optimal() { #[test] fn test_evaluate_conflict() { // Two overlapping vars assigned same register => conflict - let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3), (4, 3)]); + let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3), (4, 3)]).unwrap(); let result = problem.evaluate(&vec![0, 0, 1]).unwrap(); // Vars 0 and 1 overlap (arcs [0,3) and [2,5)), same register 0 => invalid assert_eq!(result, Min(None)); @@ -33,7 +35,7 @@ fn test_evaluate_conflict() { #[test] fn test_evaluate_non_overlapping() { // Two non-overlapping vars can share a register - let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 2), (3, 2)]); + let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 2), (3, 2)]).unwrap(); // Arcs [0,2) and [3,5) don't overlap let result = problem.evaluate(&vec![0, 0]).unwrap(); assert_eq!(result, Min(Some(1))); @@ -42,14 +44,14 @@ fn test_evaluate_non_overlapping() { #[test] fn test_evaluate_all_different() { // Trivial assignment: all different registers - let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3), (4, 3)]); + let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3), (4, 3)]).unwrap(); let result = problem.evaluate(&vec![0, 1, 2]).unwrap(); assert_eq!(result, Min(Some(3))); } #[test] fn test_evaluate_invalid_config_length() { - let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3)]); + let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3)]).unwrap(); assert!(matches!( problem.evaluate(&vec![0]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -58,7 +60,7 @@ fn test_evaluate_invalid_config_length() { #[test] fn test_evaluate_out_of_range_register() { - let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3)]); + let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3)]).unwrap(); assert!(matches!( problem.evaluate(&vec![0, 5]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -68,7 +70,7 @@ fn test_evaluate_out_of_range_register() { #[test] fn test_solver_k3() { // All pairs conflict: need 3 registers - let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3), (4, 3)]); + let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3), (4, 3)]).unwrap(); let solver = BruteForce::new(); let witness = solver.solve(&problem).unwrap().unwrap(); let value = problem.evaluate(&witness).unwrap(); @@ -78,7 +80,7 @@ fn test_solver_k3() { #[test] fn test_solver_two_non_overlapping() { // Two non-overlapping arcs: can share 1 register - let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 2), (3, 2)]); + let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 2), (3, 2)]).unwrap(); let solver = BruteForce::new(); let witness = solver.solve(&problem).unwrap().unwrap(); let value = problem.evaluate(&witness).unwrap(); @@ -88,7 +90,7 @@ fn test_solver_two_non_overlapping() { #[test] fn test_solver_two_overlapping() { // Two overlapping arcs: need 2 registers - let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 4), (3, 4)]); + let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 4), (3, 4)]).unwrap(); let solver = BruteForce::new(); let witness = solver.solve(&problem).unwrap().unwrap(); let value = problem.evaluate(&witness).unwrap(); @@ -100,7 +102,7 @@ fn test_circular_wrap_around_overlap() { // Arc (5, 3) on loop length 6 covers timesteps {5, 0, 1} // Arc (0, 3) covers timesteps {0, 1, 2} // They overlap at timesteps 0 and 1 - let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(5, 3), (0, 3)]); + let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(5, 3), (0, 3)]).unwrap(); let result = problem.evaluate(&vec![0, 0]).unwrap(); assert_eq!(result, Min(None)); // conflict let result = problem.evaluate(&vec![0, 1]).unwrap(); @@ -109,7 +111,7 @@ fn test_circular_wrap_around_overlap() { #[test] fn test_single_variable() { - let problem = MinimumRegisterSufficiencyForLoops::new(4, vec![(0, 2)]); + let problem = MinimumRegisterSufficiencyForLoops::new(4, vec![(0, 2)]).unwrap(); let solver = BruteForce::new(); let witness = solver.solve(&problem).unwrap().unwrap(); let value = problem.evaluate(&witness).unwrap(); @@ -118,7 +120,7 @@ fn test_single_variable() { #[test] fn test_serialization() { - let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3), (4, 3)]); + let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3), (4, 3)]).unwrap(); let json = serde_json::to_string(&problem).unwrap(); let deserialized: MinimumRegisterSufficiencyForLoops = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.loop_length(), 6); @@ -130,7 +132,7 @@ fn test_serialization() { fn test_paper_example() { // Paper example: N=6, vars: (0,3), (2,3), (4,3) - all pairs conflict (K3) // Config [0,1,2] -> 3 registers -> Min(3) is optimal - let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3), (4, 3)]); + let problem = MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 3), (2, 3), (4, 3)]).unwrap(); let config = vec![0, 1, 2]; let result = problem.evaluate(&config).unwrap(); assert_eq!(result, Min(Some(3))); @@ -142,19 +144,16 @@ fn test_paper_example() { } #[test] -#[should_panic(expected = "loop_length must be positive")] -fn test_zero_loop_length_panics() { - MinimumRegisterSufficiencyForLoops::new(0, vec![]); +fn test_zero_loop_length_rejects() { + assert!(MinimumRegisterSufficiencyForLoops::new(0, vec![]).is_err()); } #[test] -#[should_panic(expected = "duration")] -fn test_zero_duration_panics() { - MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 0)]); +fn test_zero_duration_rejects() { + assert!(MinimumRegisterSufficiencyForLoops::new(6, vec![(0, 0)]).is_err()); } #[test] -#[should_panic(expected = "start_time")] -fn test_invalid_start_time_panics() { - MinimumRegisterSufficiencyForLoops::new(6, vec![(6, 2)]); +fn test_invalid_start_time_rejects() { + assert!(MinimumRegisterSufficiencyForLoops::new(6, vec![(6, 2)]).is_err()); } diff --git a/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs b/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs index 635e66453..b964c4815 100644 --- a/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs +++ b/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::One; @@ -12,12 +11,16 @@ fn test_minimum_tardiness_sequencing_basic() { 5, vec![5, 5, 5, 3, 3], vec![(0, 3), (1, 3), (1, 4), (2, 4)], - ); + ) + .unwrap(); assert_eq!(problem.num_tasks(), 5); assert_eq!(problem.deadlines(), &[5, 5, 5, 3, 3]); assert_eq!(problem.precedences(), &[(0, 3), (1, 3), (1, 4), (2, 4)]); assert_eq!(problem.num_precedences(), 4); - assert_eq!(problem.dimensions(), vec![5, 4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 4, 3, 2, 1] + ); assert_eq!( as Problem>::NAME, "MinimumTardinessSequencing" @@ -30,20 +33,21 @@ fn test_minimum_tardiness_sequencing_evaluate_optimal() { 5, vec![5, 5, 5, 3, 3], vec![(0, 3), (1, 3), (1, 4), (2, 4)], - ); + ) + .unwrap(); let config = vec![0, 1, 3, 2, 4]; assert_eq!(problem.evaluate(&config).unwrap(), Min(Some(1))); } #[test] fn test_minimum_tardiness_sequencing_evaluate_duplicate_task() { - let problem = MinimumTardinessSequencing::::new(3, vec![2, 3, 1], vec![]); + let problem = MinimumTardinessSequencing::::new(3, vec![2, 3, 1], vec![]).unwrap(); assert_eq!(problem.evaluate(&vec![0, 2, 0]).unwrap(), Min(None)); } #[test] fn test_minimum_tardiness_sequencing_evaluate_out_of_range() { - let problem = MinimumTardinessSequencing::::new(3, vec![2, 3, 1], vec![]); + let problem = MinimumTardinessSequencing::::new(3, vec![2, 3, 1], vec![]).unwrap(); assert!(matches!( problem.evaluate(&vec![0, 1, 5]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -52,7 +56,7 @@ fn test_minimum_tardiness_sequencing_evaluate_out_of_range() { #[test] fn test_minimum_tardiness_sequencing_evaluate_wrong_length() { - let problem = MinimumTardinessSequencing::::new(3, vec![2, 3, 1], vec![]); + let problem = MinimumTardinessSequencing::::new(3, vec![2, 3, 1], vec![]).unwrap(); assert!(matches!( problem.evaluate(&vec![0, 1]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -65,7 +69,7 @@ fn test_minimum_tardiness_sequencing_evaluate_wrong_length() { #[test] fn test_minimum_tardiness_sequencing_evaluate_precedence_violation() { - let problem = MinimumTardinessSequencing::::new(3, vec![3, 3, 3], vec![(0, 1)]); + let problem = MinimumTardinessSequencing::::new(3, vec![3, 3, 3], vec![(0, 1)]).unwrap(); assert_eq!(problem.evaluate(&vec![0, 1, 2]).unwrap(), Min(Some(0))); assert_eq!(problem.evaluate(&vec![1, 0, 2]).unwrap(), Min(None)); assert_eq!(problem.evaluate(&vec![2, 1, 0]).unwrap(), Min(None)); @@ -73,14 +77,14 @@ fn test_minimum_tardiness_sequencing_evaluate_precedence_violation() { #[test] fn test_minimum_tardiness_sequencing_evaluate_all_on_time() { - let problem = MinimumTardinessSequencing::::new(3, vec![3, 3, 3], vec![]); + let problem = MinimumTardinessSequencing::::new(3, vec![3, 3, 3], vec![]).unwrap(); assert_eq!(problem.evaluate(&vec![0, 1, 2]).unwrap(), Min(Some(0))); assert_eq!(problem.evaluate(&vec![2, 1, 0]).unwrap(), Min(Some(0))); } #[test] fn test_minimum_tardiness_sequencing_evaluate_all_tardy() { - let problem = MinimumTardinessSequencing::::new(2, vec![0, 0], vec![]); + let problem = MinimumTardinessSequencing::::new(2, vec![0, 0], vec![]).unwrap(); assert_eq!(problem.evaluate(&vec![0, 1]).unwrap(), Min(Some(2))); } @@ -90,7 +94,8 @@ fn test_minimum_tardiness_sequencing_brute_force() { 5, vec![5, 5, 5, 3, 3], vec![(0, 3), (1, 3), (1, 4), (2, 4)], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -102,7 +107,7 @@ fn test_minimum_tardiness_sequencing_brute_force() { #[test] fn test_minimum_tardiness_sequencing_brute_force_no_precedences() { - let problem = MinimumTardinessSequencing::::new(3, vec![1, 3, 2], vec![]); + let problem = MinimumTardinessSequencing::::new(3, vec![1, 3, 2], vec![]).unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -114,7 +119,7 @@ fn test_minimum_tardiness_sequencing_brute_force_no_precedences() { #[test] fn test_minimum_tardiness_sequencing_serialization() { - let problem = MinimumTardinessSequencing::::new(3, vec![2, 3, 1], vec![(0, 1)]); + let problem = MinimumTardinessSequencing::::new(3, vec![2, 3, 1], vec![(0, 1)]).unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: MinimumTardinessSequencing = serde_json::from_value(json).unwrap(); assert_eq!(restored.num_tasks(), problem.num_tasks()); @@ -124,38 +129,43 @@ fn test_minimum_tardiness_sequencing_serialization() { #[test] fn test_minimum_tardiness_sequencing_empty() { - let problem = MinimumTardinessSequencing::::new(0, vec![], vec![]); + let problem = MinimumTardinessSequencing::::new(0, vec![], vec![]).unwrap(); assert_eq!(problem.num_tasks(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } #[test] fn test_minimum_tardiness_sequencing_single_task() { - let problem = MinimumTardinessSequencing::::new(1, vec![1], vec![]); - assert_eq!(problem.dimensions(), vec![1]); + let problem = MinimumTardinessSequencing::::new(1, vec![1], vec![]).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1] + ); assert_eq!(problem.evaluate(&vec![0]).unwrap(), Min(Some(0))); - let problem_tardy = MinimumTardinessSequencing::::new(1, vec![0], vec![]); + let problem_tardy = MinimumTardinessSequencing::::new(1, vec![0], vec![]).unwrap(); assert_eq!(problem_tardy.evaluate(&vec![0]).unwrap(), Min(Some(1))); } #[test] -#[should_panic(expected = "deadlines length must equal num_tasks")] fn test_minimum_tardiness_sequencing_mismatched_deadlines() { - MinimumTardinessSequencing::::new(3, vec![1, 2], vec![]); + assert!(MinimumTardinessSequencing::::new(3, vec![1, 2], vec![]).is_err()); } #[test] -#[should_panic(expected = "predecessor index 5 out of range")] fn test_minimum_tardiness_sequencing_invalid_precedence() { - MinimumTardinessSequencing::::new(3, vec![1, 2, 3], vec![(5, 0)]); + assert!(MinimumTardinessSequencing::::new(3, vec![1, 2, 3], vec![(5, 0)]).is_err()); } #[test] fn test_minimum_tardiness_sequencing_cyclic_precedences() { let problem = - MinimumTardinessSequencing::::new(3, vec![3, 3, 3], vec![(0, 1), (1, 2), (2, 0)]); + MinimumTardinessSequencing::::new(3, vec![3, 3, 3], vec![(0, 1), (1, 2), (2, 0)]) + .unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); } @@ -168,7 +178,8 @@ fn test_minimum_tardiness_sequencing_weighted_basic() { vec![3, 2, 2, 1, 2], vec![4, 3, 8, 3, 6], vec![(0, 2), (1, 3)], - ); + ) + .unwrap(); assert_eq!(problem.num_tasks(), 5); assert_eq!(problem.lengths(), &[3, 2, 2, 1, 2]); assert_eq!(problem.deadlines(), &[4, 3, 8, 3, 6]); @@ -183,7 +194,8 @@ fn test_minimum_tardiness_sequencing_weighted_evaluate() { vec![3, 2, 2, 1, 2], vec![4, 3, 8, 3, 6], vec![(0, 2), (1, 3)], - ); + ) + .unwrap(); // t0(l=3): finish=3, deadline=4 → on time // t4(l=2): finish=5, deadline=6 → on time // t2(l=2): finish=7, deadline=8 → on time @@ -201,7 +213,8 @@ fn test_minimum_tardiness_sequencing_weighted_brute_force() { vec![3, 2, 2, 1, 2], vec![4, 3, 8, 3, 6], vec![(0, 2), (1, 3)], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -214,7 +227,8 @@ fn test_minimum_tardiness_sequencing_weighted_brute_force() { #[test] fn test_minimum_tardiness_sequencing_weighted_serialization() { let problem = - MinimumTardinessSequencing::::with_lengths(vec![3, 2, 2], vec![4, 3, 8], vec![(0, 1)]); + MinimumTardinessSequencing::::with_lengths(vec![3, 2, 2], vec![4, 3, 8], vec![(0, 1)]) + .unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: MinimumTardinessSequencing = serde_json::from_value(json).unwrap(); assert_eq!(restored.num_tasks(), problem.num_tasks()); @@ -229,7 +243,8 @@ fn test_minimum_tardiness_sequencing_weighted_different_lengths() { // Schedule [0,1,2]: t0(l=1,fin=1≤2✓), t1(l=5,fin=6≤6✓), t2(l=1,fin=7>3✗) → 1 tardy // Schedule [1,0,2]: t1(l=5,fin=5≤6✓), t0(l=1,fin=6>2✗), t2(l=1,fin=7>3✗) → 2 tardy let problem = - MinimumTardinessSequencing::::with_lengths(vec![1, 5, 1], vec![2, 6, 3], vec![]); + MinimumTardinessSequencing::::with_lengths(vec![1, 5, 1], vec![2, 6, 3], vec![]) + .unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -239,16 +254,19 @@ fn test_minimum_tardiness_sequencing_weighted_different_lengths() { } #[test] -#[should_panic(expected = "all task lengths must be positive")] fn test_minimum_tardiness_sequencing_weighted_zero_length() { - MinimumTardinessSequencing::::with_lengths(vec![1, 0, 2], vec![3, 3, 3], vec![]); + assert!( + MinimumTardinessSequencing::::with_lengths(vec![1, 0, 2], vec![3, 3, 3], vec![]) + .is_err() + ); } #[test] fn test_minimum_tardiness_sequencing_paper_example() { // Issue example (unit-length): 4 tasks, deadlines [2,3,1,4], prec (0→2) // t0: finish=1≤2✓, t1: finish=2≤3✓, t2: finish=3>1✗, t3: finish=4≤4✓ → 1 tardy - let problem = MinimumTardinessSequencing::::new(4, vec![2, 3, 1, 4], vec![(0, 2)]); + let problem = + MinimumTardinessSequencing::::new(4, vec![2, 3, 1, 4], vec![(0, 2)]).unwrap(); assert_eq!(problem.evaluate(&vec![0, 1, 2, 3]).unwrap(), Min(Some(1))); } #[test] diff --git a/src/unit_tests/models/misc/minimum_weight_and_or_graph.rs b/src/unit_tests/models/misc/minimum_weight_and_or_graph.rs index 5cbe85f53..231175d01 100644 --- a/src/unit_tests/models/misc/minimum_weight_and_or_graph.rs +++ b/src/unit_tests/models/misc/minimum_weight_and_or_graph.rs @@ -27,6 +27,7 @@ fn issue_problem() -> MinimumWeightAndOrGraph { vec![Some(true), Some(false), Some(false), None, None, None, None], vec![1, 2, 3, 1, 4, 2], ) + .unwrap() } #[test] @@ -38,8 +39,11 @@ fn test_minimum_weight_and_or_graph_creation() { assert_eq!(problem.source(), 0); assert_eq!(problem.gate_types().len(), 7); assert_eq!(problem.arc_weights().len(), 6); - assert_eq!(problem.num_variables(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!(problem.num_variables().unwrap(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!( ::NAME, "MinimumWeightAndOrGraph" @@ -204,3 +208,16 @@ fn test_minimum_weight_and_or_graph_paper_example() { vec![true, true, false, true, false, true] ); } + +#[test] +fn deserialize_rejects_invalid_graph_before_building_outgoing_arcs() { + for (arcs, source) in [(vec![(2, 1)], 0), (vec![(0, 1)], 2)] { + assert!( + serde_json::from_value::(serde_json::json!({ + "num_vertices": 2, "arcs": arcs, "source": source, + "gate_types": [true, null], "arc_weights": [1] + })) + .is_err() + ); + } +} diff --git a/src/unit_tests/models/misc/multiprocessor_scheduling.rs b/src/unit_tests/models/misc/multiprocessor_scheduling.rs index a8581588c..33332011d 100644 --- a/src/unit_tests/models/misc/multiprocessor_scheduling.rs +++ b/src/unit_tests/models/misc/multiprocessor_scheduling.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_zero_processors() { @@ -21,14 +20,17 @@ use crate::traits::Problem; #[test] fn test_multiprocessor_scheduling_basic() { - let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); + let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10).unwrap(); assert_eq!(problem.num_tasks(), 5); assert_eq!(problem.total_length(), 20); assert_eq!(problem.lengths(), &[4, 5, 3, 2, 6]); assert_eq!(problem.num_processors(), 2); assert_eq!(problem.deadline(), 10); assert_eq!(problem.total_length(), 20); - assert_eq!(problem.dimensions(), vec![2; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); assert_eq!( ::NAME, "MultiprocessorScheduling" @@ -38,28 +40,28 @@ fn test_multiprocessor_scheduling_basic() { #[test] fn test_multiprocessor_scheduling_feasible() { - let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); + let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10).unwrap(); // Processor 0: tasks 0,4 => 4+6=10, Processor 1: tasks 1,2,3 => 5+3+2=10 assert!(problem.evaluate(&vec![0, 1, 1, 1, 0]).unwrap()); } #[test] fn test_multiprocessor_scheduling_infeasible() { - let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); + let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10).unwrap(); // Processor 0: tasks 0,1,2,3,4 => 4+5+3+2+6=20 > 10 assert!(!problem.evaluate(&vec![0, 0, 0, 0, 0]).unwrap()); } #[test] fn test_multiprocessor_scheduling_infeasible_tight() { - let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); + let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10).unwrap(); // Processor 0: tasks 0,1,4 => 4+5+6=15 > 10 assert!(!problem.evaluate(&vec![0, 0, 1, 1, 0]).unwrap()); } #[test] fn test_multiprocessor_scheduling_wrong_config_length() { - let problem = MultiprocessorScheduling::new(vec![4, 5, 3], 2, 10); + let problem = MultiprocessorScheduling::new(vec![4, 5, 3], 2, 10).unwrap(); assert!(matches!( problem.evaluate(&vec![0, 1]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -72,7 +74,7 @@ fn test_multiprocessor_scheduling_wrong_config_length() { #[test] fn test_multiprocessor_scheduling_invalid_processor_index() { - let problem = MultiprocessorScheduling::new(vec![4, 5, 3], 2, 10); + let problem = MultiprocessorScheduling::new(vec![4, 5, 3], 2, 10).unwrap(); // Processor index 2 is out of range for 2 processors assert!(matches!( problem.evaluate(&vec![0, 2, 0]), @@ -82,31 +84,37 @@ fn test_multiprocessor_scheduling_invalid_processor_index() { #[test] fn test_multiprocessor_scheduling_empty_instance() { - let problem = MultiprocessorScheduling::new(vec![], 2, 10); + let problem = MultiprocessorScheduling::new(vec![], 2, 10).unwrap(); assert_eq!(problem.num_tasks(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); // Empty assignment is always feasible assert!(problem.evaluate(&vec![]).unwrap()); } #[test] fn test_multiprocessor_scheduling_single_task() { - let problem = MultiprocessorScheduling::new(vec![5], 2, 5); + let problem = MultiprocessorScheduling::new(vec![5], 2, 5).unwrap(); assert!(problem.evaluate(&vec![0]).unwrap()); assert!(problem.evaluate(&vec![1]).unwrap()); } #[test] fn test_multiprocessor_scheduling_single_task_exceeds_deadline() { - let problem = MultiprocessorScheduling::new(vec![11], 2, 10); + let problem = MultiprocessorScheduling::new(vec![11], 2, 10).unwrap(); assert!(!problem.evaluate(&vec![0]).unwrap()); assert!(!problem.evaluate(&vec![1]).unwrap()); } #[test] fn test_multiprocessor_scheduling_three_processors() { - let problem = MultiprocessorScheduling::new(vec![3, 3, 3], 3, 3); - assert_eq!(problem.dimensions(), vec![3; 3]); + let problem = MultiprocessorScheduling::new(vec![3, 3, 3], 3, 3).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 3] + ); // One task per processor assert!(problem.evaluate(&vec![0, 1, 2]).unwrap()); // Two tasks on one processor exceeds deadline @@ -115,7 +123,7 @@ fn test_multiprocessor_scheduling_three_processors() { #[test] fn test_multiprocessor_scheduling_brute_force() { - let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); + let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10).unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); assert!(solution.is_some()); @@ -126,7 +134,7 @@ fn test_multiprocessor_scheduling_brute_force() { #[test] 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 problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 9).unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); assert!(solution.is_none()); @@ -136,7 +144,7 @@ fn test_multiprocessor_scheduling_brute_force_infeasible() { 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 problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); for sol in &solutions { @@ -152,14 +160,14 @@ fn test_multiprocessor_scheduling_find_all_witnesses() { 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 problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 9).unwrap(); let solver = BruteForce::new(); assert!(solver.find_all_witnesses(&problem).unwrap().is_empty()); } #[test] fn test_multiprocessor_scheduling_serialization() { - let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); + let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10).unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: MultiprocessorScheduling = serde_json::from_value(json).unwrap(); assert_eq!(restored.lengths(), problem.lengths()); @@ -176,23 +184,22 @@ fn test_multiprocessor_scheduling_deserialization_rejects_zero_processors() { })) .unwrap_err(); assert!( - err.to_string().contains("expected positive integer, got 0"), + err.to_string().contains("num_processors must be positive"), "unexpected error: {err}" ); } #[test] -#[should_panic(expected = "num_processors must be positive")] fn test_multiprocessor_scheduling_zero_processors() { - MultiprocessorScheduling::new(vec![1, 2], 0, 5); + assert!(MultiprocessorScheduling::new(vec![1, 2], 0, 5).is_err()); } #[test] fn test_multiprocessor_scheduling_deadline_zero() { // Only feasible if all lengths are 0 - let problem = MultiprocessorScheduling::new(vec![0, 0], 2, 0); + let problem = MultiprocessorScheduling::new(vec![0, 0], 2, 0).unwrap(); assert!(problem.evaluate(&vec![0, 1]).unwrap()); - let problem2 = MultiprocessorScheduling::new(vec![1, 0], 2, 0); + let problem2 = MultiprocessorScheduling::new(vec![1, 0], 2, 0).unwrap(); assert!(!problem2.evaluate(&vec![0, 1]).unwrap()); } diff --git a/src/unit_tests/models/misc/non_liveness_free_petri_net.rs b/src/unit_tests/models/misc/non_liveness_free_petri_net.rs index 987c583b8..66ba4a44b 100644 --- a/src/unit_tests/models/misc/non_liveness_free_petri_net.rs +++ b/src/unit_tests/models/misc/non_liveness_free_petri_net.rs @@ -29,8 +29,11 @@ fn test_non_liveness_free_petri_net_basic() { assert_eq!(problem.num_transitions(), 3); assert_eq!(problem.num_arcs(), 6); assert_eq!(problem.initial_token_sum(), 1); - assert_eq!(problem.dimensions(), vec![2; 3]); - assert_eq!(problem.num_variables(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 3] + ); + assert_eq!(problem.num_variables().unwrap(), 3); assert_eq!( ::NAME, "NonLivenessFreePetriNet" diff --git a/src/unit_tests/models/misc/numerical_3_dimensional_matching.rs b/src/unit_tests/models/misc/numerical_3_dimensional_matching.rs index aa0f97684..cfbe42bb8 100644 --- a/src/unit_tests/models/misc/numerical_3_dimensional_matching.rs +++ b/src/unit_tests/models/misc/numerical_3_dimensional_matching.rs @@ -18,8 +18,11 @@ fn test_numerical_3dm_creation() { assert_eq!(problem.sizes_y(), &[5, 7]); assert_eq!(problem.bound(), 15); assert_eq!(problem.num_groups(), 2); - assert_eq!(problem.dimensions(), vec![2; 4]); - assert_eq!(problem.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); + assert_eq!(problem.num_variables().unwrap(), 4); assert_eq!( ::NAME, "Numerical3DimensionalMatching" diff --git a/src/unit_tests/models/misc/numerical_matching_with_target_sums.rs b/src/unit_tests/models/misc/numerical_matching_with_target_sums.rs index c2eb0221e..417969a15 100644 --- a/src/unit_tests/models/misc/numerical_matching_with_target_sums.rs +++ b/src/unit_tests/models/misc/numerical_matching_with_target_sums.rs @@ -17,8 +17,11 @@ fn test_nmts_creation() { assert_eq!(problem.sizes_y(), &[2, 5, 3]); assert_eq!(problem.targets(), &[3, 7, 12]); assert_eq!(problem.num_pairs(), 3); - assert_eq!(problem.dimensions(), vec![3; 3]); - assert_eq!(problem.num_variables(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 3] + ); + assert_eq!(problem.num_variables().unwrap(), 3); assert_eq!( ::NAME, "NumericalMatchingWithTargetSums" diff --git a/src/unit_tests/models/misc/open_shop_scheduling.rs b/src/unit_tests/models/misc/open_shop_scheduling.rs index a80a35455..6abacfbe6 100644 --- a/src/unit_tests/models/misc/open_shop_scheduling.rs +++ b/src/unit_tests/models/misc/open_shop_scheduling.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -60,10 +59,16 @@ fn test_open_shop_scheduling_creation() { #[test] fn test_open_shop_scheduling_dims() { let p = issue_example(); - assert_eq!(p.dimensions(), vec![24usize; 12]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![24usize; 12] + ); let p2 = two_by_two(); - assert_eq!(p2.dimensions(), vec![7usize; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p2).unwrap(), + vec![7usize; 4] + ); } // ─── evaluate ──────────────────────────────────────────────────────────────── @@ -119,7 +124,10 @@ fn test_open_shop_scheduling_evaluate_wrong_length() { #[test] fn test_open_shop_scheduling_evaluate_empty() { let p = OpenShopScheduling::new(3, vec![]); - assert_eq!(p.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + Vec::::new() + ); assert_eq!(p.evaluate(&vec![]).unwrap(), Min(Some(0))); } diff --git a/src/unit_tests/models/misc/optimum_communication_spanning_tree.rs b/src/unit_tests/models/misc/optimum_communication_spanning_tree.rs index ef3555fd7..382b081bf 100644 --- a/src/unit_tests/models/misc/optimum_communication_spanning_tree.rs +++ b/src/unit_tests/models/misc/optimum_communication_spanning_tree.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_edge_weights() { @@ -28,7 +27,7 @@ fn k4_problem() -> OptimumCommunicationSpanningTree { vec![1, 1, 0, 2], vec![3, 1, 2, 0], ]; - OptimumCommunicationSpanningTree::new(edge_weights, requirements) + OptimumCommunicationSpanningTree::new(edge_weights, requirements).unwrap() } #[test] @@ -36,7 +35,10 @@ fn test_ocst_creation() { let problem = k4_problem(); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!( ::NAME, "OptimumCommunicationSpanningTree" @@ -157,7 +159,7 @@ fn test_ocst_k3_equal_requirements() { // edge_weights: w(0,1)=1, w(0,2)=2, w(1,2)=3 let edge_weights = vec![vec![0, 1, 2], vec![1, 0, 3], vec![2, 3, 0]]; let requirements = vec![vec![0, 1, 1], vec![1, 0, 1], vec![1, 1, 0]]; - let problem = OptimumCommunicationSpanningTree::new(edge_weights, requirements); + let problem = OptimumCommunicationSpanningTree::new(edge_weights, requirements).unwrap(); assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_edges(), 3); @@ -183,27 +185,26 @@ fn test_ocst_k3_equal_requirements() { } #[test] -#[should_panic(expected = "must have at least 2 vertices")] -fn test_ocst_single_vertex_panics() { - OptimumCommunicationSpanningTree::new(vec![vec![0]], vec![vec![0]]); +fn test_ocst_single_vertex_rejects() { + assert!(OptimumCommunicationSpanningTree::new(vec![vec![0]], vec![vec![0]]).is_err()); } #[test] -#[should_panic(expected = "edge_weights must be symmetric")] -fn test_ocst_asymmetric_weights_panics() { - OptimumCommunicationSpanningTree::new( +fn test_ocst_asymmetric_weights_rejects() { + assert!(OptimumCommunicationSpanningTree::new( vec![vec![0, 1], vec![2, 0]], vec![vec![0, 1], vec![1, 0]], - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "requirements must be symmetric")] -fn test_ocst_asymmetric_requirements_panics() { - OptimumCommunicationSpanningTree::new( +fn test_ocst_asymmetric_requirements_rejects() { + assert!(OptimumCommunicationSpanningTree::new( vec![vec![0, 1], vec![1, 0]], vec![vec![0, 1], vec![2, 0]], - ); + ) + .is_err()); } #[cfg(feature = "example-db")] diff --git a/src/unit_tests/models/misc/paintshop.rs b/src/unit_tests/models/misc/paintshop.rs index 93fce4912..3afef1cf5 100644 --- a/src/unit_tests/models/misc/paintshop.rs +++ b/src/unit_tests/models/misc/paintshop.rs @@ -1,20 +1,20 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; -include!("../../jl_helpers.rs"); #[test] fn test_paintshop_creation() { - let problem = PaintShop::new(vec!["a", "b", "a", "b"]); + let problem = PaintShop::new(vec!["a", "b", "a", "b"]).unwrap(); assert_eq!(problem.num_cars(), 2); assert_eq!(problem.sequence_len(), 4); - assert_eq!(problem.num_variables(), 2); + assert_eq!(problem.num_variables().unwrap(), 2); } #[test] fn test_is_first() { - let problem = PaintShop::new(vec!["a", "b", "a", "b"]); + let problem = PaintShop::new(vec!["a", "b", "a", "b"]).unwrap(); // First occurrence: a at 0, b at 1 // Second occurrence: a at 2, b at 3 assert_eq!(problem.is_first, vec![true, true, false, false]); @@ -22,7 +22,7 @@ fn test_is_first() { #[test] fn test_get_coloring() { - let problem = PaintShop::new(vec!["a", "b", "a", "b"]); + let problem = PaintShop::new(vec!["a", "b", "a", "b"]).unwrap(); // Config: a=0, b=1 // Sequence: a(0), b(1), a(1-opposite), b(0-opposite) let coloring = problem.get_coloring(&[false, true]).unwrap(); @@ -35,7 +35,7 @@ fn test_get_coloring() { #[test] fn test_get_coloring_rejects_wrong_assignment_length() { - let problem = PaintShop::new(vec!["a", "b", "a", "b"]); + let problem = PaintShop::new(vec!["a", "b", "a", "b"]).unwrap(); assert!(matches!( problem.get_coloring(&[false]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -44,7 +44,7 @@ fn test_get_coloring_rejects_wrong_assignment_length() { #[test] fn test_count_switches() { - let problem = PaintShop::new(vec!["a", "b", "a", "b"]); + let problem = PaintShop::new(vec!["a", "b", "a", "b"]).unwrap(); // Config [0, 1] -> coloring [0, 1, 1, 0] -> 2 switches assert_eq!(problem.count_switches(&[false, true]).unwrap(), 2); @@ -66,7 +66,7 @@ fn test_count_paint_switches_function() { #[test] fn test_single_car() { - let problem = PaintShop::new(vec!["a", "a"]); + let problem = PaintShop::new(vec!["a", "a"]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -80,7 +80,7 @@ fn test_single_car() { #[test] fn test_adjacent_same_car() { // Sequence: a, a, b, b - let problem = PaintShop::new(vec!["a", "a", "b", "b"]); + let problem = PaintShop::new(vec!["a", "a", "b", "b"]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -92,15 +92,14 @@ fn test_adjacent_same_car() { } #[test] -#[should_panic] fn test_invalid_sequence_single_occurrence() { - // This should panic because 'c' only appears once - let _ = PaintShop::new(vec!["a", "b", "a", "c"]); + // This is rejected because 'c' only appears once + assert!(PaintShop::new(vec!["a", "b", "a", "c"]).is_err()); } #[test] fn test_car_labels() { - let problem = PaintShop::new(vec!["car1", "car2", "car1", "car2"]); + let problem = PaintShop::new(vec!["car1", "car2", "car1", "car2"]).unwrap(); assert_eq!(problem.car_labels().len(), 2); } @@ -115,7 +114,7 @@ fn test_jl_parity_evaluation() { .iter() .map(|v| v.as_str().unwrap().to_string()) .collect(); - let problem = PaintShop::new(sequence); + let problem = PaintShop::new(sequence).unwrap(); for eval in instance["evaluations"].as_array().unwrap() { let config = jl_parse_bool_config(&eval["config"]); let result = problem.evaluate(&config).unwrap(); @@ -136,7 +135,7 @@ fn test_jl_parity_evaluation() { #[test] fn test_parameter_getters() { - let problem = PaintShop::new(vec!["a", "b", "a", "b"]); + let problem = PaintShop::new(vec!["a", "b", "a", "b"]).unwrap(); assert_eq!(problem.num_sequence(), 4); assert_eq!(problem.num_cars(), 2); } @@ -144,7 +143,7 @@ fn test_parameter_getters() { #[test] fn test_paintshop_paper_example() { // Paper: sequence (A,B,A,C,B,C), optimal 2 color changes - let problem = PaintShop::new(vec!["A", "B", "A", "C", "B", "C"]); + let problem = PaintShop::new(vec!["A", "B", "A", "C", "B", "C"]).unwrap(); assert_eq!(problem.num_cars(), 3); // Car order: A=0, B=1, C=2 (sorted) @@ -154,3 +153,26 @@ fn test_paintshop_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap().unwrap(), 2); } + +#[test] +fn deserialize_rebuilds_occurrence_metadata() { + let expected = PaintShop::new(vec!["a", "b", "a", "b"]).unwrap(); + let mut json = serde_json::to_value(&expected).unwrap(); + json["is_first"] = serde_json::json!([false]); + json["num_cars"] = serde_json::json!(99); + let restored: PaintShop = serde_json::from_value(json).unwrap(); + assert_eq!( + serde_json::to_value(restored).unwrap(), + serde_json::to_value(expected).unwrap() + ); +} + +#[test] +fn deserialize_rejects_invalid_car_indices_and_counts() { + for indices in [vec![0, 1], vec![0]] { + assert!(serde_json::from_value::(serde_json::json!({ + "sequence_indices": indices, "car_labels": ["a"] + })) + .is_err()); + } +} diff --git a/src/unit_tests/models/misc/partially_ordered_knapsack.rs b/src/unit_tests/models/misc/partially_ordered_knapsack.rs index 58794aeaf..2cda6a3e8 100644 --- a/src/unit_tests/models/misc/partially_ordered_knapsack.rs +++ b/src/unit_tests/models/misc/partially_ordered_knapsack.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; /// Helper: create the example instance from the issue. @@ -16,6 +15,7 @@ fn example_instance() -> PartiallyOrderedKnapsack { vec![(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)], 11, ) + .unwrap() } #[test] @@ -29,7 +29,10 @@ fn test_partially_ordered_knapsack_basic() { &[(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)] ); assert_eq!(problem.capacity(), 11); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!( ::NAME, "PartiallyOrderedKnapsack" @@ -166,17 +169,21 @@ fn test_partially_ordered_knapsack_brute_force() { #[test] fn test_partially_ordered_knapsack_empty_instance() { - let problem = PartiallyOrderedKnapsack::new(vec![], vec![], vec![], 10); + let problem = PartiallyOrderedKnapsack::new(vec![], vec![], vec![], 10).unwrap(); assert_eq!(problem.num_items(), 0); assert_eq!(problem.num_precedences(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), Max(Some(0))); } #[test] 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 problem = + PartiallyOrderedKnapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], vec![], 7).unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -189,7 +196,7 @@ fn test_partially_ordered_knapsack_no_precedences() { #[test] fn test_partially_ordered_knapsack_zero_capacity() { - let problem = PartiallyOrderedKnapsack::new(vec![1, 2], vec![10, 20], vec![(0, 1)], 0); + let problem = PartiallyOrderedKnapsack::new(vec![1, 2], vec![10, 20], vec![(0, 1)], 0).unwrap(); assert_eq!(problem.evaluate(&vec![false, false]).unwrap(), Max(Some(0))); assert_eq!(problem.evaluate(&vec![true, false]).unwrap(), Max(None)); let solver = BruteForce::new(); @@ -209,44 +216,39 @@ fn test_partially_ordered_knapsack_serialization() { } #[test] -#[should_panic(expected = "weights and values must have the same length")] fn test_partially_ordered_knapsack_mismatched_lengths() { - PartiallyOrderedKnapsack::new(vec![1, 2], vec![3], vec![], 5); + assert!(PartiallyOrderedKnapsack::new(vec![1, 2], vec![3], vec![], 5).is_err()); } #[test] -#[should_panic(expected = "precedence index 5 out of bounds")] fn test_partially_ordered_knapsack_invalid_precedence() { - PartiallyOrderedKnapsack::new(vec![1, 2], vec![3, 4], vec![(0, 5)], 5); + assert!(PartiallyOrderedKnapsack::new(vec![1, 2], vec![3, 4], vec![(0, 5)], 5).is_err()); } #[test] -#[should_panic(expected = "precedences contain a cycle")] fn test_partially_ordered_knapsack_cycle() { - PartiallyOrderedKnapsack::new( + assert!(PartiallyOrderedKnapsack::new( vec![1, 2, 3], vec![1, 2, 3], vec![(0, 1), (1, 2), (2, 0)], 10, - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "capacity must be non-negative")] fn test_partially_ordered_knapsack_negative_capacity() { - PartiallyOrderedKnapsack::new(vec![1, 2], vec![3, 4], vec![], -1); + assert!(PartiallyOrderedKnapsack::new(vec![1, 2], vec![3, 4], vec![], -1).is_err()); } #[test] -#[should_panic(expected = "weight[1] must be non-negative")] fn test_partially_ordered_knapsack_negative_weight() { - PartiallyOrderedKnapsack::new(vec![1, -2], vec![3, 4], vec![], 5); + assert!(PartiallyOrderedKnapsack::new(vec![1, -2], vec![3, 4], vec![], 5).is_err()); } #[test] -#[should_panic(expected = "value[0] must be non-negative")] fn test_partially_ordered_knapsack_negative_value() { - PartiallyOrderedKnapsack::new(vec![1, 2], vec![-3, 4], vec![], 5); + assert!(PartiallyOrderedKnapsack::new(vec![1, 2], vec![-3, 4], vec![], 5).is_err()); } #[test] fn create_spec_defaults_precedences_to_empty() { diff --git a/src/unit_tests/models/misc/partition.rs b/src/unit_tests/models/misc/partition.rs index f6a969235..94a032b38 100644 --- a/src/unit_tests/models/misc/partition.rs +++ b/src/unit_tests/models/misc/partition.rs @@ -1,6 +1,5 @@ use crate::models::misc::Partition; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -9,7 +8,10 @@ fn test_partition_basic() { assert_eq!(problem.num_elements(), 6); assert_eq!(problem.sizes(), &[3, 1, 1, 2, 2, 1]); assert_eq!(problem.total_sum(), 10); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); } #[test] diff --git a/src/unit_tests/models/misc/precedence_constrained_scheduling.rs b/src/unit_tests/models/misc/precedence_constrained_scheduling.rs index 47cf34313..46153f520 100644 --- a/src/unit_tests/models/misc/precedence_constrained_scheduling.rs +++ b/src/unit_tests/models/misc/precedence_constrained_scheduling.rs @@ -1,16 +1,18 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] fn test_precedence_constrained_scheduling_basic() { - let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]); + let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]).unwrap(); assert_eq!(problem.num_tasks(), 4); assert_eq!(problem.num_processors(), 2); assert_eq!(problem.deadline(), 3); assert_eq!(problem.precedences(), &[(0, 2), (1, 3)]); - assert_eq!(problem.dimensions(), vec![3; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 4] + ); assert_eq!( ::NAME, "PrecedenceConstrainedScheduling" @@ -40,7 +42,8 @@ fn test_precedence_constrained_scheduling_evaluate_valid() { (5, 7), (6, 7), ], - ); + ) + .unwrap(); // Valid schedule: slot 0: {t0, t1}, slot 1: {t2, t3, t4}, slot 2: {t5, t6}, slot 3: {t7} let config = vec![0, 0, 1, 1, 1, 2, 2, 3]; assert!(problem.evaluate(&config).unwrap()); @@ -49,20 +52,20 @@ fn test_precedence_constrained_scheduling_evaluate_valid() { #[test] fn test_precedence_constrained_scheduling_evaluate_invalid_precedence() { // t0 < t1, but we assign both to slot 0 - let problem = PrecedenceConstrainedScheduling::new(2, 2, 3, vec![(0, 1)]); + let problem = PrecedenceConstrainedScheduling::new(2, 2, 3, vec![(0, 1)]).unwrap(); assert!(!problem.evaluate(&vec![0, 0]).unwrap()); // slot[1] = 0 < slot[0] + 1 = 1 } #[test] fn test_precedence_constrained_scheduling_evaluate_invalid_capacity() { // 3 tasks, 2 processors, all in slot 0 - let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![]); + let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![]).unwrap(); assert!(!problem.evaluate(&vec![0, 0, 0]).unwrap()); // 3 tasks in slot 0, capacity 2 } #[test] fn test_precedence_constrained_scheduling_evaluate_wrong_config_length() { - let problem = PrecedenceConstrainedScheduling::new(3, 2, 3, vec![]); + let problem = PrecedenceConstrainedScheduling::new(3, 2, 3, vec![]).unwrap(); assert!(matches!( problem.evaluate(&vec![0, 1]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -75,7 +78,7 @@ fn test_precedence_constrained_scheduling_evaluate_wrong_config_length() { #[test] fn test_precedence_constrained_scheduling_evaluate_invalid_variable_value() { - let problem = PrecedenceConstrainedScheduling::new(2, 2, 3, vec![]); + let problem = PrecedenceConstrainedScheduling::new(2, 2, 3, vec![]).unwrap(); assert!(matches!( problem.evaluate(&vec![0, 3]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -85,7 +88,7 @@ fn test_precedence_constrained_scheduling_evaluate_invalid_variable_value() { #[test] fn test_precedence_constrained_scheduling_brute_force() { // Small instance: 3 tasks, 2 processors, deadline 2, t0 < t2 - let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]); + let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]).unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -96,7 +99,7 @@ fn test_precedence_constrained_scheduling_brute_force() { #[test] fn test_precedence_constrained_scheduling_brute_force_all() { - let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]); + let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -108,14 +111,14 @@ fn test_precedence_constrained_scheduling_brute_force_all() { #[test] 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 problem = PrecedenceConstrainedScheduling::new(3, 1, 2, vec![(0, 1), (1, 2)]).unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); } #[test] fn test_precedence_constrained_scheduling_serialization() { - let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]); + let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]).unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: PrecedenceConstrainedScheduling = serde_json::from_value(json).unwrap(); assert_eq!(restored.num_tasks(), problem.num_tasks()); @@ -127,16 +130,19 @@ fn test_precedence_constrained_scheduling_serialization() { #[test] fn test_precedence_constrained_scheduling_empty() { - let problem = PrecedenceConstrainedScheduling::new(0, 1, 1, vec![]); + let problem = PrecedenceConstrainedScheduling::new(0, 1, 1, vec![]).unwrap(); assert_eq!(problem.num_tasks(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } #[test] fn test_precedence_constrained_scheduling_no_precedences() { // 4 tasks, 2 processors, deadline 2, no precedences - let problem = PrecedenceConstrainedScheduling::new(4, 2, 2, vec![]); + let problem = PrecedenceConstrainedScheduling::new(4, 2, 2, vec![]).unwrap(); // 2 tasks per slot, 2 slots = 4 tasks assert!(problem.evaluate(&vec![0, 0, 1, 1]).unwrap()); let solver = BruteForce::new(); diff --git a/src/unit_tests/models/misc/preemptive_scheduling.rs b/src/unit_tests/models/misc/preemptive_scheduling.rs index 299bb177c..c300f06fe 100644 --- a/src/unit_tests/models/misc/preemptive_scheduling.rs +++ b/src/unit_tests/models/misc/preemptive_scheduling.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -29,7 +28,10 @@ fn test_preemptive_scheduling_creation() { assert_eq!(p.lengths(), &[2, 1, 3]); assert_eq!(p.precedences(), &[(0, 2)]); assert_eq!(p.d_max(), 6); - assert_eq!(p.dimensions(), vec![2; 3 * 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2; 3 * 6] + ); assert_eq!( ::NAME, "PreemptiveScheduling" @@ -42,7 +44,10 @@ fn test_preemptive_scheduling_empty_tasks() { let p = PreemptiveScheduling::new(vec![], 1, vec![]).unwrap(); assert_eq!(p.num_tasks(), 0); assert_eq!(p.d_max(), 0); - assert_eq!(p.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + Vec::::new() + ); assert_eq!(p.evaluate(&vec![]).unwrap(), Min(Some(0))); } diff --git a/src/unit_tests/models/misc/production_planning.rs b/src/unit_tests/models/misc/production_planning.rs index 1141a3d07..351e9213c 100644 --- a/src/unit_tests/models/misc/production_planning.rs +++ b/src/unit_tests/models/misc/production_planning.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_period_vector_mismatch() { @@ -29,6 +28,7 @@ fn issue_example_problem() -> ProductionPlanning { vec![1, 1, 1, 1, 1, 1], 80, ) + .unwrap() } fn tiny_solver_problem() -> ProductionPlanning { @@ -41,6 +41,7 @@ fn tiny_solver_problem() -> ProductionPlanning { vec![0, 0, 0], 5, ) + .unwrap() } #[test] @@ -54,7 +55,10 @@ fn test_production_planning_creation() { assert_eq!(problem.inventory_costs(), &[1, 1, 1, 1, 1, 1]); assert_eq!(problem.cost_bound(), 80); assert_eq!(problem.max_capacity(), 12); - assert_eq!(problem.dimensions(), vec![13; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![13; 6] + ); assert_eq!(::NAME, "ProductionPlanning"); assert_eq!(::variant(), vec![]); } @@ -141,9 +145,8 @@ fn test_production_planning_serialization() { } #[test] -#[should_panic(expected = "all per-period vectors must have length num_periods")] fn test_production_planning_rejects_length_mismatch() { - ProductionPlanning::new( + assert!(ProductionPlanning::new( 2, vec![1], vec![1, 1], @@ -151,11 +154,11 @@ fn test_production_planning_rejects_length_mismatch() { vec![1, 1], vec![1, 1], 3, - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "num_periods must be positive")] fn test_production_planning_rejects_zero_periods() { - ProductionPlanning::new(0, vec![], vec![], vec![], vec![], vec![], 0); + assert!(ProductionPlanning::new(0, vec![], vec![], vec![], vec![], vec![], 0).is_err()); } diff --git a/src/unit_tests/models/misc/rectilinear_picture_compression.rs b/src/unit_tests/models/misc/rectilinear_picture_compression.rs index 00acdec63..552c8d812 100644 --- a/src/unit_tests/models/misc/rectilinear_picture_compression.rs +++ b/src/unit_tests/models/misc/rectilinear_picture_compression.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; fn two_block_matrix() -> Vec> { @@ -26,7 +25,7 @@ fn issue_matrix() -> Vec> { #[test] fn test_rectilinear_picture_compression_basic() { - let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2).unwrap(); assert_eq!(problem.num_rows(), 4); assert_eq!(problem.num_cols(), 4); assert_eq!(problem.bound(), 2); @@ -42,7 +41,7 @@ fn test_rectilinear_picture_compression_basic() { #[test] fn test_rectilinear_picture_compression_maximal_rectangles_two_blocks() { - let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2).unwrap(); let rects = problem.maximal_rectangles(); // Two disjoint 2x2 blocks: (0,0,1,1) and (2,2,3,3) assert_eq!(rects, vec![(0, 0, 1, 1), (2, 2, 3, 3)]); @@ -50,21 +49,24 @@ fn test_rectilinear_picture_compression_maximal_rectangles_two_blocks() { #[test] fn test_rectilinear_picture_compression_dims() { - let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2).unwrap(); // 2 maximal rectangles -> 2 binary variables - assert_eq!(problem.dimensions(), vec![2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2] + ); } #[test] fn test_rectilinear_picture_compression_evaluate_satisfying() { - let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2).unwrap(); // Select both maximal rectangles assert!(problem.evaluate(&vec![true, true]).unwrap()); } #[test] fn test_rectilinear_picture_compression_evaluate_unsatisfying_not_all_covered() { - let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2).unwrap(); // Select only first rectangle - second block uncovered assert!(!problem.evaluate(&vec![true, false]).unwrap()); // Select only second rectangle - first block uncovered @@ -75,14 +77,14 @@ fn test_rectilinear_picture_compression_evaluate_unsatisfying_not_all_covered() #[test] fn test_rectilinear_picture_compression_evaluate_bound_exceeded() { - let problem = RectilinearPictureCompression::new(two_block_matrix(), 1); + let problem = RectilinearPictureCompression::new(two_block_matrix(), 1).unwrap(); // Both selected but bound is 1 assert!(!problem.evaluate(&vec![true, true]).unwrap()); } #[test] fn test_rectilinear_picture_compression_evaluate_wrong_config_length() { - let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2).unwrap(); assert!(matches!( problem.evaluate(&vec![true]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -95,7 +97,7 @@ fn test_rectilinear_picture_compression_evaluate_wrong_config_length() { #[test] fn test_rectilinear_picture_compression_evaluate_invalid_variable_value() { - let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2).unwrap(); assert!( crate::registry::DynProblem::evaluate_dyn(&problem, &serde_json::json!([2, false])) .is_err() @@ -104,7 +106,7 @@ fn test_rectilinear_picture_compression_evaluate_invalid_variable_value() { #[test] fn test_rectilinear_picture_compression_issue_matrix_satisfiable() { - let problem = RectilinearPictureCompression::new(issue_matrix(), 3); + let problem = RectilinearPictureCompression::new(issue_matrix(), 3).unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); assert!(solution.is_some()); @@ -114,7 +116,7 @@ fn test_rectilinear_picture_compression_issue_matrix_satisfiable() { #[test] fn test_rectilinear_picture_compression_issue_matrix_unsatisfiable() { - let problem = RectilinearPictureCompression::new(issue_matrix(), 2); + let problem = RectilinearPictureCompression::new(issue_matrix(), 2).unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); assert!(solution.is_none()); @@ -122,7 +124,7 @@ fn test_rectilinear_picture_compression_issue_matrix_unsatisfiable() { #[test] fn test_rectilinear_picture_compression_brute_force() { - let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2).unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -133,7 +135,7 @@ fn test_rectilinear_picture_compression_brute_force() { #[test] fn test_rectilinear_picture_compression_brute_force_all() { - let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); // Two disjoint 2x2 blocks with K=2: exactly one satisfying config [1,1]. @@ -145,7 +147,7 @@ fn test_rectilinear_picture_compression_brute_force_all() { #[test] fn test_rectilinear_picture_compression_serialization() { - let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); + let problem = RectilinearPictureCompression::new(two_block_matrix(), 2).unwrap(); let json = serde_json::to_value(&problem).unwrap(); assert_eq!( json, @@ -170,10 +172,13 @@ fn test_rectilinear_picture_compression_serialization() { fn test_rectilinear_picture_compression_single_cell() { // Single 1-entry matrix let matrix = vec![vec![true]]; - let problem = RectilinearPictureCompression::new(matrix, 1); + let problem = RectilinearPictureCompression::new(matrix, 1).unwrap(); let rects = problem.maximal_rectangles(); assert_eq!(rects, vec![(0, 0, 0, 0)]); - assert_eq!(problem.dimensions(), vec![2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2] + ); assert!(problem.evaluate(&vec![true]).unwrap()); assert!(!problem.evaluate(&vec![false]).unwrap()); } @@ -182,10 +187,13 @@ fn test_rectilinear_picture_compression_single_cell() { fn test_rectilinear_picture_compression_all_zeros() { // Matrix with no 1-entries: no maximal rectangles, always satisfiable let matrix = vec![vec![false, false], vec![false, false]]; - let problem = RectilinearPictureCompression::new(matrix, 0); + let problem = RectilinearPictureCompression::new(matrix, 0).unwrap(); let rects = problem.maximal_rectangles(); assert!(rects.is_empty()); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); // Empty config satisfies (no 1-entries to cover) assert!(problem.evaluate(&vec![]).unwrap()); } @@ -194,7 +202,7 @@ fn test_rectilinear_picture_compression_all_zeros() { fn test_rectilinear_picture_compression_full_matrix() { // 2x2 all-ones matrix: one maximal rectangle covers everything let matrix = vec![vec![true, true], vec![true, true]]; - let problem = RectilinearPictureCompression::new(matrix, 1); + let problem = RectilinearPictureCompression::new(matrix, 1).unwrap(); let rects = problem.maximal_rectangles(); assert_eq!(rects, vec![(0, 0, 1, 1)]); assert!(problem.evaluate(&vec![true]).unwrap()); @@ -205,7 +213,7 @@ fn test_rectilinear_picture_compression_full_matrix() { fn test_rectilinear_picture_compression_overlapping_rectangles() { // L-shaped region: requires multiple rectangles, some may overlap let matrix = vec![vec![true, true], vec![true, false]]; - let problem = RectilinearPictureCompression::new(matrix, 2); + let problem = RectilinearPictureCompression::new(matrix, 2).unwrap(); let rects = problem.maximal_rectangles(); // Maximal rectangles: (0,0,1,0) vertical bar, (0,0,0,1) horizontal bar assert!(rects.contains(&(0, 0, 1, 0))); @@ -218,24 +226,33 @@ fn test_rectilinear_picture_compression_overlapping_rectangles() { #[test] fn test_rectilinear_picture_compression_matrix_getter() { let matrix = two_block_matrix(); - let problem = RectilinearPictureCompression::new(matrix.clone(), 2); + let problem = RectilinearPictureCompression::new(matrix.clone(), 2).unwrap(); assert_eq!(problem.matrix(), &matrix); } #[test] -#[should_panic(expected = "empty")] -fn test_rectilinear_picture_compression_empty_matrix_panics() { - RectilinearPictureCompression::new(vec![], 1); +fn test_rectilinear_picture_compression_empty_matrix_rejects() { + assert!(RectilinearPictureCompression::new(vec![], 1).is_err()); } #[test] -#[should_panic(expected = "column")] -fn test_rectilinear_picture_compression_empty_row_panics() { - RectilinearPictureCompression::new(vec![vec![]], 1); +fn test_rectilinear_picture_compression_empty_row_rejects() { + assert!(RectilinearPictureCompression::new(vec![vec![]], 1).is_err()); } #[test] -#[should_panic(expected = "same length")] -fn test_rectilinear_picture_compression_inconsistent_rows_panics() { - RectilinearPictureCompression::new(vec![vec![true, false], vec![true]], 1); +fn test_rectilinear_picture_compression_inconsistent_rows_rejects() { + assert!(RectilinearPictureCompression::new(vec![vec![true, false], vec![true]], 1).is_err()); +} + +#[test] +fn deserialize_rejects_invalid_matrix_before_building_rectangles() { + for matrix in [vec![], vec![vec![]], vec![vec![true], vec![]]] { + assert!( + serde_json::from_value::(serde_json::json!({ + "matrix": matrix, "bound": 1 + })) + .is_err() + ); + } } diff --git a/src/unit_tests/models/misc/register_sufficiency.rs b/src/unit_tests/models/misc/register_sufficiency.rs index a112f8bc8..8d86c17ac 100644 --- a/src/unit_tests/models/misc/register_sufficiency.rs +++ b/src/unit_tests/models/misc/register_sufficiency.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -18,7 +17,8 @@ fn test_register_sufficiency_basic() { (6, 5), ], 3, - ); + ) + .unwrap(); assert_eq!(problem.num_vertices(), 7); assert_eq!(problem.num_arcs(), 8); assert_eq!(problem.bound(), 3); @@ -35,7 +35,10 @@ fn test_register_sufficiency_basic() { (6, 5) ] ); - assert_eq!(problem.dimensions(), vec![7; 7]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![7; 7] + ); assert_eq!( ::NAME, "RegisterSufficiency" @@ -59,7 +62,8 @@ fn test_register_sufficiency_evaluate_valid() { (6, 5), ], 3, - ); + ) + .unwrap(); // Order: v0,v1,v2,v3,v5,v4,v6 (0-indexed) // Positions: v0->0, v1->1, v2->2, v3->3, v4->5, v5->4, v6->6 let config = vec![0, 1, 2, 3, 5, 4, 6]; @@ -72,7 +76,7 @@ fn test_register_sufficiency_evaluate_valid() { #[test] fn test_register_sufficiency_evaluate_invalid_permutation() { - let problem = RegisterSufficiency::new(4, vec![(2, 0), (3, 0), (3, 1)], 2); + let problem = RegisterSufficiency::new(4, vec![(2, 0), (3, 0), (3, 1)], 2).unwrap(); // Not a permutation: position 0 used twice assert!(!problem.evaluate(&vec![0, 0, 1, 2]).unwrap()); // Wrong length @@ -94,7 +98,7 @@ fn test_register_sufficiency_evaluate_invalid_permutation() { #[test] fn test_register_sufficiency_evaluate_invalid_dependency() { // v2 depends on v0, v3 depends on v0 and v1 - let problem = RegisterSufficiency::new(4, vec![(2, 0), (3, 0), (3, 1)], 4); + let problem = RegisterSufficiency::new(4, vec![(2, 0), (3, 0), (3, 1)], 4).unwrap(); // v2 at position 0, v0 at position 1 -> v2 evaluated before its dependency v0 assert!(!problem.evaluate(&vec![1, 2, 0, 3]).unwrap()); } @@ -115,7 +119,8 @@ fn test_register_sufficiency_evaluate_exceeds_bound() { (6, 5), ], 2, - ); + ) + .unwrap(); // Same valid ordering but K=2 is too small let config = vec![0, 1, 2, 3, 5, 4, 6]; assert!(!problem.evaluate(&config).unwrap()); @@ -124,7 +129,7 @@ fn test_register_sufficiency_evaluate_exceeds_bound() { #[test] fn test_register_sufficiency_brute_force() { // Small instance: 4 vertices, v2 depends on v0, v3 depends on v1 - let problem = RegisterSufficiency::new(4, vec![(2, 0), (3, 1)], 2); + let problem = RegisterSufficiency::new(4, vec![(2, 0), (3, 1)], 2).unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -135,7 +140,7 @@ fn test_register_sufficiency_brute_force() { #[test] fn test_register_sufficiency_brute_force_all() { - let problem = RegisterSufficiency::new(4, vec![(2, 0), (3, 1)], 2); + let problem = RegisterSufficiency::new(4, vec![(2, 0), (3, 1)], 2).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -150,7 +155,7 @@ fn test_register_sufficiency_unsatisfiable() { // Plus: v3 also depends on v0 // This requires 3 registers (v0 must stay alive until v3) // With K=1, impossible - let problem = RegisterSufficiency::new(4, vec![(1, 0), (2, 1), (3, 2), (3, 0)], 1); + let problem = RegisterSufficiency::new(4, vec![(1, 0), (2, 1), (3, 2), (3, 0)], 1).unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); } @@ -170,7 +175,8 @@ fn test_register_sufficiency_serialization() { (6, 5), ], 3, - ); + ) + .unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: RegisterSufficiency = serde_json::from_value(json).unwrap(); assert_eq!(restored.num_vertices(), problem.num_vertices()); @@ -181,18 +187,21 @@ fn test_register_sufficiency_serialization() { #[test] fn test_register_sufficiency_empty() { - let problem = RegisterSufficiency::new(0, vec![], 0); + let problem = RegisterSufficiency::new(0, vec![], 0).unwrap(); assert_eq!(problem.num_vertices(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } #[test] fn test_register_sufficiency_single_vertex() { - let problem = RegisterSufficiency::new(1, vec![], 1); + let problem = RegisterSufficiency::new(1, vec![], 1).unwrap(); assert!(problem.evaluate(&vec![0]).unwrap()); // K=0 should fail (vertex needs one register) - let problem_k0 = RegisterSufficiency::new(1, vec![], 0); + let problem_k0 = RegisterSufficiency::new(1, vec![], 0).unwrap(); assert!(!problem_k0.evaluate(&vec![0]).unwrap()); } @@ -212,7 +221,8 @@ fn test_register_sufficiency_paper_example() { (6, 5), ], 3, - ); + ) + .unwrap(); // The order from the issue: v1,v2,v3,v4,v6,v5,v7 (1-indexed) // = v0,v1,v2,v3,v5,v4,v6 (0-indexed) @@ -235,7 +245,8 @@ fn test_register_sufficiency_paper_example() { (6, 5), ], 2, - ); + ) + .unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem_k2).unwrap().is_none()); } diff --git a/src/unit_tests/models/misc/resource_constrained_scheduling.rs b/src/unit_tests/models/misc/resource_constrained_scheduling.rs index 70415a3b0..45c71ee8f 100644 --- a/src/unit_tests/models/misc/resource_constrained_scheduling.rs +++ b/src/unit_tests/models/misc/resource_constrained_scheduling.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -17,9 +16,17 @@ fn test_resource_constrained_scheduling_creation() { assert_eq!(problem.resource_bounds(), &[20]); assert_eq!(problem.deadline(), 2); assert_eq!(problem.num_resources(), 1); - assert_eq!(problem.dimensions().len(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 6 + ); // Each variable has domain {0, 1} (deadline = 2) - assert!(problem.dimensions().iter().all(|&d| d == 2)); + assert!(crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .iter() + .all(|&d| d == 2)); } #[test] @@ -115,7 +122,10 @@ fn test_resource_constrained_scheduling_empty_tasks() { let problem = ResourceConstrainedScheduling::new(2, vec![10], Vec::>::new(), 3).unwrap(); assert_eq!(problem.num_tasks(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } diff --git a/src/unit_tests/models/misc/scheduling_to_minimize_weighted_completion_time.rs b/src/unit_tests/models/misc/scheduling_to_minimize_weighted_completion_time.rs index 54fd389aa..9ad8c1347 100644 --- a/src/unit_tests/models/misc/scheduling_to_minimize_weighted_completion_time.rs +++ b/src/unit_tests/models/misc/scheduling_to_minimize_weighted_completion_time.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_task_weights() { @@ -28,7 +27,10 @@ fn test_scheduling_min_wct_creation() { assert_eq!(problem.num_processors(), 2); assert_eq!(problem.lengths(), &[1, 2, 3, 4, 5]); assert_eq!(problem.weights(), &[6, 4, 3, 2, 1]); - assert_eq!(problem.dimensions(), vec![2; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); assert_eq!( ::NAME, "SchedulingToMinimizeWeightedCompletionTime" @@ -195,7 +197,10 @@ fn test_scheduling_min_wct_single_processor() { #[test] fn test_scheduling_min_wct_three_processors() { let problem = SchedulingToMinimizeWeightedCompletionTime::new(vec![3, 3, 3], vec![1, 1, 1], 3); - assert_eq!(problem.dimensions(), vec![3; 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 3] + ); // One task per processor: each completes at 3, WCT = 3*1 + 3*1 + 3*1 = 9 assert_eq!(problem.evaluate(&vec![0, 1, 2]).unwrap(), Min(Some(9))); // All on one processor: C(t0)=3, C(t1)=6, C(t2)=9, WCT = 3+6+9 = 18 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 3a017d7a4..cdcbdf14c 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,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_deadline_count_mismatch() { @@ -27,6 +26,7 @@ fn issue_example_problem() -> SchedulingWithIndividualDeadlines { vec![2, 1, 2, 2, 3, 3, 2], vec![(0, 3), (1, 3), (1, 4), (2, 4), (2, 5)], ) + .unwrap() } #[test] @@ -42,7 +42,10 @@ fn test_scheduling_with_individual_deadlines_basic() { ); assert_eq!(problem.num_precedences(), 5); assert_eq!(problem.max_deadline(), 3); - assert_eq!(problem.dimensions(), vec![2, 1, 2, 2, 3, 3, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 1, 2, 2, 3, 3, 2] + ); assert_eq!( ::NAME, "SchedulingWithIndividualDeadlines" @@ -97,7 +100,7 @@ fn test_scheduling_with_individual_deadlines_evaluate_rejects_capacity_violation #[test] fn test_scheduling_with_individual_deadlines_evaluate_handles_huge_sparse_deadline() { - let problem = SchedulingWithIndividualDeadlines::new(1, 1, vec![i64::MAX], vec![]); + let problem = SchedulingWithIndividualDeadlines::new(1, 1, vec![i64::MAX], vec![]).unwrap(); let result = std::panic::catch_unwind(|| problem.evaluate(&vec![0]).unwrap()); @@ -106,7 +109,7 @@ fn test_scheduling_with_individual_deadlines_evaluate_handles_huge_sparse_deadli #[test] fn test_scheduling_with_individual_deadlines_slots_can_exceed_task_count() { - let problem = SchedulingWithIndividualDeadlines::new(1, 1, vec![3], vec![]); + let problem = SchedulingWithIndividualDeadlines::new(1, 1, vec![3], vec![]).unwrap(); assert!(problem.evaluate(&vec![1]).unwrap()); assert_eq!( BruteForce::new().find_all_witnesses(&problem).unwrap(), @@ -118,7 +121,8 @@ fn test_scheduling_with_individual_deadlines_slots_can_exceed_task_count() { #[test] fn test_scheduling_with_individual_deadlines_brute_force_satisfiable() { - let problem = SchedulingWithIndividualDeadlines::new(3, 2, vec![1, 1, 2], vec![(0, 2)]); + let problem = + SchedulingWithIndividualDeadlines::new(3, 2, vec![1, 1, 2], vec![(0, 2)]).unwrap(); let solver = BruteForce::new(); assert_eq!( @@ -130,7 +134,7 @@ fn test_scheduling_with_individual_deadlines_brute_force_satisfiable() { #[test] fn test_scheduling_with_individual_deadlines_brute_force_unsatisfiable() { - let problem = SchedulingWithIndividualDeadlines::new(3, 1, vec![1, 1, 1], vec![]); + let problem = SchedulingWithIndividualDeadlines::new(3, 1, vec![1, 1, 1], vec![]).unwrap(); let solver = BruteForce::new(); assert!(solver.solve(&problem).unwrap().is_none()); @@ -164,15 +168,13 @@ fn test_scheduling_with_individual_deadlines_paper_example() { } #[test] -#[should_panic(expected = "deadlines length must equal num_tasks")] fn test_scheduling_with_individual_deadlines_mismatched_deadlines() { - SchedulingWithIndividualDeadlines::new(2, 1, vec![1], vec![]); + assert!(SchedulingWithIndividualDeadlines::new(2, 1, vec![1], vec![]).is_err()); } #[test] -#[should_panic(expected = "predecessor index 4 out of range")] fn test_scheduling_with_individual_deadlines_invalid_precedence() { - SchedulingWithIndividualDeadlines::new(3, 2, vec![1, 1, 1], vec![(4, 1)]); + assert!(SchedulingWithIndividualDeadlines::new(3, 2, vec![1, 1, 1], vec![(4, 1)]).is_err()); } #[test] fn create_spec_defaults_precedences_to_empty() { 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 de3ae8efe..49cb187fc 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,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_precedences() { @@ -32,7 +31,10 @@ fn test_sequencing_to_minimize_maximum_cumulative_cost_creation() { ); assert_eq!(problem.num_tasks(), 6); assert_eq!(problem.num_precedences(), 6); - assert_eq!(problem.dimensions(), vec![6, 5, 4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6, 5, 4, 3, 2, 1] + ); assert_eq!( ::NAME, "SequencingToMinimizeMaximumCumulativeCost" @@ -128,7 +130,10 @@ fn test_sequencing_to_minimize_maximum_cumulative_cost_solver_aggregate() { fn test_sequencing_to_minimize_maximum_cumulative_cost_empty_instance() { let problem = SequencingToMinimizeMaximumCumulativeCost::new(vec![], vec![]); assert_eq!(problem.num_tasks(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); // Empty schedule: no tasks, max cumulative cost is 0. let val = problem.evaluate(&vec![]).unwrap(); assert_eq!(val, Min(Some(0))); diff --git a/src/unit_tests/models/misc/sequencing_to_minimize_tardy_task_weight.rs b/src/unit_tests/models/misc/sequencing_to_minimize_tardy_task_weight.rs index feae2b123..739b1530c 100644 --- a/src/unit_tests/models/misc/sequencing_to_minimize_tardy_task_weight.rs +++ b/src/unit_tests/models/misc/sequencing_to_minimize_tardy_task_weight.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_task_weights() { @@ -29,7 +28,10 @@ fn test_sequencing_to_minimize_tardy_task_weight_basic() { assert_eq!(problem.lengths(), &[3, 2, 4, 1, 2]); assert_eq!(problem.weights(), &[5, 3, 7, 2, 4]); assert_eq!(problem.deadlines(), &[6, 4, 10, 2, 8]); - assert_eq!(problem.dimensions(), vec![5, 5, 5, 5, 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 5, 5, 5, 5] + ); assert_eq!( ::NAME, "SequencingToMinimizeTardyTaskWeight" @@ -187,7 +189,10 @@ fn test_sequencing_to_minimize_tardy_task_weight_deserialization_rejects_zero_we #[test] fn test_sequencing_to_minimize_tardy_task_weight_single_task() { let problem = SequencingToMinimizeTardyTaskWeight::new(vec![3], vec![2], vec![5]); - assert_eq!(problem.dimensions(), vec![1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1] + ); // completes at 3, deadline 5, on time assert_eq!(problem.evaluate(&vec![0]).unwrap(), Min(Some(0))); } @@ -203,7 +208,10 @@ fn test_sequencing_to_minimize_tardy_task_weight_single_task_tardy() { fn test_sequencing_to_minimize_tardy_task_weight_empty() { let problem = SequencingToMinimizeTardyTaskWeight::new(vec![], vec![], vec![]); assert_eq!(problem.num_tasks(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } 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 97c31b24d..688705c4c 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,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -17,7 +16,10 @@ fn test_sequencing_to_minimize_weighted_completion_time_basic() { assert_eq!(problem.weights(), &[3, 5, 1, 4, 2]); assert_eq!(problem.precedences(), &[(0, 2), (1, 4)]); assert_eq!(problem.num_precedences(), 2); - assert_eq!(problem.dimensions(), vec![5, 4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 4, 3, 2, 1] + ); assert_eq!( ::NAME, "SequencingToMinimizeWeightedCompletionTime" @@ -129,7 +131,10 @@ fn test_sequencing_to_minimize_weighted_completion_time_empty() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![], vec![], vec![]); assert_eq!(problem.num_tasks(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } @@ -137,7 +142,10 @@ fn test_sequencing_to_minimize_weighted_completion_time_empty() { fn test_sequencing_to_minimize_weighted_completion_time_single_task() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![3], vec![2], vec![]); - assert_eq!(problem.dimensions(), vec![1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1] + ); assert_eq!(problem.evaluate(&vec![0]).unwrap(), Min(Some(6))); } 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 3214d9c90..cb18c02f2 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,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_vector_length_mismatch() { @@ -27,6 +26,7 @@ fn issue_example_yes() -> SequencingToMinimizeWeightedTardiness { vec![5, 8, 4, 15, 10], 13, ) + .unwrap() } fn issue_example_no() -> SequencingToMinimizeWeightedTardiness { @@ -36,6 +36,7 @@ fn issue_example_no() -> SequencingToMinimizeWeightedTardiness { vec![5, 8, 4, 15, 10], 12, ) + .unwrap() } #[test] @@ -47,7 +48,10 @@ fn test_sequencing_to_minimize_weighted_tardiness_basic() { assert_eq!(problem.deadlines(), &[5, 8, 4, 15, 10]); assert_eq!(problem.bound(), 13); assert_eq!(problem.num_tasks(), 5); - assert_eq!(problem.dimensions(), vec![5, 4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 4, 3, 2, 1] + ); assert_eq!( ::NAME, "SequencingToMinimizeWeightedTardiness" @@ -74,7 +78,8 @@ fn test_sequencing_to_minimize_weighted_tardiness_reports_overflow() { vec![1, 1], vec![0, 0], i64::MAX, - ); + ) + .unwrap(); assert!(matches!( problem.evaluate(&vec![0, 1]), Err(crate::traits::EvaluationError::IntegerOverflow(_)) diff --git a/src/unit_tests/models/misc/sequencing_with_deadlines_and_set_up_times.rs b/src/unit_tests/models/misc/sequencing_with_deadlines_and_set_up_times.rs index 26f9f9f0a..7d406a2e1 100644 --- a/src/unit_tests/models/misc/sequencing_with_deadlines_and_set_up_times.rs +++ b/src/unit_tests/models/misc/sequencing_with_deadlines_and_set_up_times.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Or; @@ -19,7 +18,10 @@ fn test_sequencing_with_deadlines_and_set_up_times_creation() { assert_eq!(problem.deadlines(), &[4, 11, 3, 16, 7]); assert_eq!(problem.compilers(), &[0, 1, 0, 1, 0]); assert_eq!(problem.setup_times(), &[1, 2]); - assert_eq!(problem.dimensions(), vec![5, 5, 5, 5, 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 5, 5, 5, 5] + ); assert_eq!( ::NAME, "SequencingWithDeadlinesAndSetUpTimes" 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 1c86ed2d8..e27c56d9e 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,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -9,14 +8,18 @@ fn test_sequencing_rtd_basic() { vec![3, 2, 4, 1, 2], vec![0, 1, 5, 0, 8], vec![5, 6, 10, 3, 12], - ); + ) + .unwrap(); assert_eq!(problem.num_tasks(), 5); assert_eq!(problem.lengths(), &[3, 2, 4, 1, 2]); assert_eq!(problem.release_times(), &[0, 1, 5, 0, 8]); assert_eq!(problem.deadlines(), &[5, 6, 10, 3, 12]); assert_eq!(problem.time_horizon(), 12); // Lehmer code dims: [5, 4, 3, 2, 1] - assert_eq!(problem.dimensions(), vec![5, 4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 4, 3, 2, 1] + ); assert_eq!( ::NAME, "SequencingWithReleaseTimesAndDeadlines" @@ -34,7 +37,8 @@ fn test_sequencing_rtd_evaluate_feasible() { vec![3, 2, 4, 1, 2], vec![0, 1, 5, 0, 8], vec![5, 6, 10, 3, 12], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); // Exactly one feasible schedule exists: [3, 0, 1, 2, 4]. @@ -48,7 +52,8 @@ fn test_sequencing_rtd_evaluate_infeasible_deadline() { vec![3, 2], vec![0, 0], vec![2, 4], // task 0 needs 3 time units but deadline is 2 - ); + ) + .unwrap(); // Order [0, 1]: t0 start=0, finish=3 > 2 -> infeasible assert!(!problem.evaluate(&vec![0, 1]).unwrap()); // Order [1, 0]: t1 start=0, finish=2; t0 start=2, finish=5 > 2 -> infeasible @@ -57,7 +62,8 @@ fn test_sequencing_rtd_evaluate_infeasible_deadline() { #[test] fn test_sequencing_rtd_evaluate_wrong_config_length() { - let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![0, 0], vec![2, 2]); + let problem = + SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![0, 0], vec![2, 2]).unwrap(); assert!(matches!( problem.evaluate(&vec![0]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -70,17 +76,23 @@ fn test_sequencing_rtd_evaluate_wrong_config_length() { #[test] fn test_sequencing_rtd_empty_instance() { - let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![], vec![], vec![]); + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![], vec![], vec![]).unwrap(); assert_eq!(problem.num_tasks(), 0); assert_eq!(problem.time_horizon(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } #[test] fn test_sequencing_rtd_single_task() { - let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2], vec![1], vec![5]); - assert_eq!(problem.dimensions(), vec![1]); + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2], vec![1], vec![5]).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1] + ); // Only one permutation: task 0 starts at max(1,0)=1, finish=3 <= 5 assert!(problem.evaluate(&vec![0]).unwrap()); } @@ -89,7 +101,8 @@ fn test_sequencing_rtd_single_task() { fn test_sequencing_rtd_brute_force() { // Small instance: 3 tasks that fit tightly let problem = - SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]); + SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]) + .unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -100,7 +113,8 @@ fn test_sequencing_rtd_brute_force() { #[test] fn test_sequencing_rtd_brute_force_all() { - let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![0, 0], vec![3, 3]); + let problem = + SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![0, 0], vec![3, 3]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -112,7 +126,8 @@ fn test_sequencing_rtd_brute_force_all() { #[test] 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 problem = + SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 0], vec![3, 3]).unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); assert!(solution.is_none()); @@ -121,7 +136,8 @@ fn test_sequencing_rtd_unsatisfiable() { #[test] fn test_sequencing_rtd_serialization() { let problem = - SequencingWithReleaseTimesAndDeadlines::new(vec![3, 2, 4], vec![0, 1, 5], vec![5, 6, 10]); + SequencingWithReleaseTimesAndDeadlines::new(vec![3, 2, 4], vec![0, 1, 5], vec![5, 6, 10]) + .unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: SequencingWithReleaseTimesAndDeadlines = serde_json::from_value(json).unwrap(); assert_eq!(restored.lengths(), problem.lengths()); @@ -132,7 +148,8 @@ fn test_sequencing_rtd_serialization() { #[test] fn test_sequencing_rtd_tight_schedule() { // Tasks that can only be scheduled in one specific order - let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 2], vec![2, 4]); + let problem = + SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 2], vec![2, 4]).unwrap(); // Order [0, 1]: t0 start=max(0,0)=0, finish=2<=2; t1 start=max(2,2)=2, finish=4<=4 ✓ assert!(problem.evaluate(&vec![0, 1]).unwrap()); // Order [1, 0]: t1 start=max(2,0)=2, finish=4<=4; t0 start=max(0,4)=4, finish=6>2 ✗ @@ -141,7 +158,8 @@ fn test_sequencing_rtd_tight_schedule() { #[test] fn test_sequencing_rtd_invalid_task_index() { - let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![0, 0], vec![2, 2]); + let problem = + SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![0, 0], vec![2, 2]).unwrap(); // Task index 2 is outside 0..2. assert!(matches!( problem.evaluate(&vec![2, 0]), diff --git a/src/unit_tests/models/misc/sequencing_within_intervals.rs b/src/unit_tests/models/misc/sequencing_within_intervals.rs index d040834e4..2bd472838 100644 --- a/src/unit_tests/models/misc/sequencing_within_intervals.rs +++ b/src/unit_tests/models/misc/sequencing_within_intervals.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_accepts_empty_window() { @@ -37,7 +36,10 @@ fn test_sequencing_within_intervals_creation() { // Task 2: 9 - 3 - 2 + 1 = 5 // Task 3: 12 - 6 - 3 + 1 = 4 // Task 4: 12 - 0 - 2 + 1 = 11 - assert_eq!(problem.dimensions(), vec![4, 6, 5, 4, 11]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 6, 5, 4, 11] + ); } #[test] @@ -152,7 +154,10 @@ fn test_sequencing_within_intervals_empty() { let problem = SequencingWithinIntervals::new(vec![], vec![], vec![]).unwrap(); assert_eq!(problem.num_tasks(), 0); assert_eq!(problem.num_start_slots(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } @@ -174,7 +179,10 @@ fn test_sequencing_within_intervals_variant() { fn test_sequencing_within_intervals_single_task() { let problem = SequencingWithinIntervals::new(vec![0], vec![5], vec![3]).unwrap(); // dims = 5 - 0 - 3 + 1 = 3 - assert_eq!(problem.dimensions(), vec![3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3] + ); // Any valid config should be feasible (only one task, no overlaps possible) assert!(problem.evaluate(&vec![0]).unwrap()); assert!(problem.evaluate(&vec![1]).unwrap()); @@ -217,7 +225,10 @@ fn test_sequencing_within_intervals_empty_start_domain() { .unwrap(); let restored: SequencingWithinIntervals = serde_json::from_value(serde_json::to_value(&problem).unwrap()).unwrap(); - assert_eq!(restored.dimensions(), vec![2, 0]); + assert_eq!( + crate::solvers::cartesian_dimensions(&restored).unwrap(), + vec![2, 0] + ); assert_eq!(restored.num_start_slots(), 2); let (value, witnesses) = BruteForce::new().solve_with_witnesses(&restored).unwrap(); assert_eq!(value, crate::types::Or(false)); diff --git a/src/unit_tests/models/misc/shortest_common_supersequence.rs b/src/unit_tests/models/misc/shortest_common_supersequence.rs index 65dfccfcb..f23d6052a 100644 --- a/src/unit_tests/models/misc/shortest_common_supersequence.rs +++ b/src/unit_tests/models/misc/shortest_common_supersequence.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -65,12 +64,16 @@ fn test_shortestcommonsupersequence_basic() { let problem = ShortestCommonSupersequence::new( 3, vec![vec![0, 1, 2, 1], vec![1, 2, 0, 1], vec![0, 2, 1, 0]], - ); + ) + .unwrap(); assert_eq!(problem.alphabet_size(), 3); assert_eq!(problem.num_strings(), 3); assert_eq!(problem.max_length(), 12); // 4+4+4 assert_eq!(problem.total_length(), 12); - assert_eq!(problem.dimensions(), vec![4; 12]); // alphabet_size+1 = 4, max_length = 12 + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 12] + ); // alphabet_size+1 = 4, max_length = 12 assert_eq!( ::NAME, "ShortestCommonSupersequence" @@ -87,7 +90,8 @@ fn test_shortestcommonsupersequence_evaluate_valid() { let problem = ShortestCommonSupersequence::new( 3, vec![vec![0, 1, 2, 1], vec![1, 2, 0, 1], vec![0, 2, 1, 0]], - ); + ) + .unwrap(); let mut config = vec![ Some(0), Some(1), @@ -106,7 +110,8 @@ fn test_shortestcommonsupersequence_evaluate_infeasible() { let problem = ShortestCommonSupersequence::new( 3, vec![vec![0, 1, 2, 1], vec![1, 2, 0, 1], vec![0, 2, 1, 0]], - ); + ) + .unwrap(); // All zeros padded: [0,0,0,0,0,0,0, 3,3,3,3,3] cannot contain [0,1,2,1] let mut config = vec![Some(0); 7]; config.extend(vec![None; 5]); @@ -115,7 +120,7 @@ fn test_shortestcommonsupersequence_evaluate_infeasible() { #[test] fn test_shortestcommonsupersequence_out_of_range() { - let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1]]); + let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1]]).unwrap(); // max_length = 2, config must have 2 entries // value 3 is out of range (alphabet_size=2, padding=2, so valid symbols are 0,1,2) // Actually 3 > alphabet_size so treated as invalid (not padding) @@ -128,7 +133,7 @@ fn test_shortestcommonsupersequence_out_of_range() { #[test] fn test_shortestcommonsupersequence_wrong_length() { - let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1]]); + let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1]]).unwrap(); // max_length = 2, wrong config lengths return None assert!(matches!( problem.evaluate(&vec![Some(0)]), @@ -143,7 +148,7 @@ fn test_shortestcommonsupersequence_wrong_length() { #[test] fn test_shortestcommonsupersequence_interleaved_padding() { // Padding must be contiguous at the end - let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1]]); + let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1]]).unwrap(); assert_eq!(problem.evaluate(&vec![None, Some(0)]).unwrap(), Min(None)); } @@ -152,7 +157,7 @@ fn test_shortestcommonsupersequence_brute_force() { // alphabet {0,1}, strings [0,1] and [1,0] // max_length = 4, search space = 3^4 = 81 // Optimal SCS length = 3 (e.g. [0,1,0] or [1,0,1]) - let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]]); + let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]]).unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -165,7 +170,7 @@ fn test_shortestcommonsupersequence_brute_force() { #[test] fn test_shortestcommonsupersequence_solve_aggregate() { - let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]]); + let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]]).unwrap(); let solver = BruteForce::new(); let val_solution = solver.solve(&problem).unwrap().unwrap(); let val = problem.evaluate(&val_solution).unwrap(); @@ -176,7 +181,7 @@ fn test_shortestcommonsupersequence_solve_aggregate() { fn test_shortestcommonsupersequence_all_padding() { // All padding = effective length 0 = empty supersequence // Only valid if all input strings are empty - let problem = ShortestCommonSupersequence::new(2, vec![vec![]]); + let problem = ShortestCommonSupersequence::new(2, vec![vec![]]).unwrap(); // max_length = 0, so config is empty assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } @@ -185,7 +190,7 @@ fn test_shortestcommonsupersequence_all_padding() { fn test_shortestcommonsupersequence_single_string() { // Single string [0,1,2] over ternary alphabet // max_length = 3, search space = 4^3 = 64 - let problem = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2]]); + let problem = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2]]).unwrap(); // [0,1,2] with no padding = the string itself, length 3 assert_eq!( problem.evaluate(&vec![Some(0), Some(1), Some(2)]).unwrap(), @@ -202,7 +207,7 @@ fn test_shortestcommonsupersequence_single_string() { fn test_shortestcommonsupersequence_find_all_witnesses() { // alphabet {0,1}, strings [0,1] and [1,0] // max_length = 4, search space = 3^4 = 81 - let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]]); + let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); for sol in &solutions { @@ -216,7 +221,7 @@ fn test_shortestcommonsupersequence_find_all_witnesses() { #[test] fn test_shortestcommonsupersequence_serialization() { - let problem = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2], vec![2, 1, 0]]); + let problem = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2], vec![2, 1, 0]]).unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: ShortestCommonSupersequence = serde_json::from_value(json).unwrap(); assert_eq!(restored.alphabet_size(), problem.alphabet_size()); @@ -228,7 +233,7 @@ fn test_shortestcommonsupersequence_serialization() { fn test_shortestcommonsupersequence_paper_example() { // Paper: Sigma = {a, b, c}, R = {"abc", "bac"}, supersequence "babc" (length 4) // Mapping: a=0, b=1, c=2 - let problem = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2], vec![1, 0, 2]]); + let problem = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2], vec![1, 0, 2]]).unwrap(); // max_length = 3 + 3 = 6, padding = 3 // "babc" = [1, 0, 1, 2] padded to [1, 0, 1, 2, 3, 3] assert_eq!( diff --git a/src/unit_tests/models/misc/shortest_common_superstring.rs b/src/unit_tests/models/misc/shortest_common_superstring.rs index c2462e6c3..6c645455c 100644 --- a/src/unit_tests/models/misc/shortest_common_superstring.rs +++ b/src/unit_tests/models/misc/shortest_common_superstring.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -21,12 +20,16 @@ fn padded_solution(values: Vec, padding: usize) -> Vec> { #[test] fn test_shortestcommonsuperstring_basic() { let problem = - ShortestCommonSuperstring::new(3, vec![vec![0, 1, 2], vec![1, 2, 0], vec![2, 0, 1]]); + ShortestCommonSuperstring::new(3, vec![vec![0, 1, 2], vec![1, 2, 0], vec![2, 0, 1]]) + .unwrap(); assert_eq!(problem.alphabet_size(), 3); assert_eq!(problem.num_strings(), 3); assert_eq!(problem.max_length(), 9); // 3+3+3 assert_eq!(problem.total_length(), 9); - assert_eq!(problem.dimensions(), vec![4; 9]); // alphabet_size + 1 = 4 across max_length = 9 positions + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 9] + ); // alphabet_size + 1 = 4 across max_length = 9 positions assert_eq!( ::NAME, "ShortestCommonSuperstring" @@ -49,7 +52,8 @@ fn test_shortestcommonsuperstring_evaluate_valid_substring() { vec![1, 2, 2], // bcc vec![2, 2, 0], // cca ], - ); + ) + .unwrap(); let pad = 3; let mut config = vec![0, 0, 1, 2, 0, 1, 2, 2, 0]; // "aabcabcca" config.extend(vec![pad; problem.max_length() - 9]); @@ -63,7 +67,7 @@ fn test_shortestcommonsuperstring_evaluate_subsequence_not_substring() { // is NOT a valid superstring. Take strings [0,1] and [1,0] and try w = [0,1,0] // (length 3) -- valid. But w = [0,2,1,0] (length 4) is also valid because // "01" is NOT a contiguous substring of "0210". Confirm "01" does not appear. - let problem = ShortestCommonSuperstring::new(3, vec![vec![0, 1], vec![1, 0]]); + let problem = ShortestCommonSuperstring::new(3, vec![vec![0, 1], vec![1, 0]]).unwrap(); // w = [0,2,1,0] padded: "01" is not a contiguous substring -> invalid let pad = 3; let mut config = vec![0, 2, 1, 0]; @@ -85,7 +89,8 @@ fn test_shortestcommonsuperstring_evaluate_subsequence_not_substring() { #[test] fn test_shortestcommonsuperstring_evaluate_infeasible() { let problem = - ShortestCommonSuperstring::new(3, vec![vec![0, 1, 2], vec![1, 2, 0], vec![2, 0, 1]]); + ShortestCommonSuperstring::new(3, vec![vec![0, 1, 2], vec![1, 2, 0], vec![2, 0, 1]]) + .unwrap(); // All zeros padded cannot contain [0,1,2]. let pad = 3; let mut config = vec![0; 9]; @@ -98,7 +103,7 @@ fn test_shortestcommonsuperstring_evaluate_infeasible() { #[test] fn test_shortestcommonsuperstring_out_of_range() { - let problem = ShortestCommonSuperstring::new(2, vec![vec![0, 1]]); + let problem = ShortestCommonSuperstring::new(2, vec![vec![0, 1]]).unwrap(); // max_length = 2. Value 3 is neither a valid symbol (0..2) nor padding (= 2). assert!(matches!( problem.evaluate(&vec![Some(0), Some(3)]), @@ -108,7 +113,7 @@ fn test_shortestcommonsuperstring_out_of_range() { #[test] fn test_shortestcommonsuperstring_wrong_length() { - let problem = ShortestCommonSuperstring::new(2, vec![vec![0, 1]]); + let problem = ShortestCommonSuperstring::new(2, vec![vec![0, 1]]).unwrap(); assert!(matches!( problem.evaluate(&vec![Some(0)]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -122,7 +127,7 @@ fn test_shortestcommonsuperstring_wrong_length() { #[test] fn test_shortestcommonsuperstring_interleaved_padding() { // Padding must be contiguous at the end. - let problem = ShortestCommonSuperstring::new(2, vec![vec![0, 1]]); + let problem = ShortestCommonSuperstring::new(2, vec![vec![0, 1]]).unwrap(); assert_eq!(problem.evaluate(&vec![None, Some(0)]).unwrap(), Min(None)); } @@ -131,7 +136,7 @@ fn test_shortestcommonsuperstring_brute_force_small() { // Alphabet {0, 1}, strings [0,1] and [1,0]. // max_length = 4, search space = 3^4 = 81. // Optimal superstring length = 3 (e.g. "010" or "101"). - let problem = ShortestCommonSuperstring::new(2, vec![vec![0, 1], vec![1, 0]]); + let problem = ShortestCommonSuperstring::new(2, vec![vec![0, 1], vec![1, 0]]).unwrap(); let solver = BruteForce::new(); let witness = solver .solve(&problem) @@ -143,7 +148,7 @@ fn test_shortestcommonsuperstring_brute_force_small() { #[test] fn test_shortestcommonsuperstring_solve_aggregate() { - let problem = ShortestCommonSuperstring::new(2, vec![vec![0, 1], vec![1, 0]]); + let problem = ShortestCommonSuperstring::new(2, vec![vec![0, 1], vec![1, 0]]).unwrap(); let solver = BruteForce::new(); let val_solution = solver.solve(&problem).unwrap().unwrap(); let val = problem.evaluate(&val_solution).unwrap(); @@ -152,7 +157,7 @@ fn test_shortestcommonsuperstring_solve_aggregate() { #[test] fn test_shortestcommonsuperstring_serialization() { - let problem = ShortestCommonSuperstring::new(3, vec![vec![0, 1, 2], vec![2, 1, 0]]); + let problem = ShortestCommonSuperstring::new(3, vec![vec![0, 1, 2], vec![2, 1, 0]]).unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: ShortestCommonSuperstring = serde_json::from_value(json).unwrap(); assert_eq!(restored.alphabet_size(), problem.alphabet_size()); @@ -176,7 +181,8 @@ fn test_shortestcommonsuperstring_example1_ternary() { vec![1, 2, 2], // bcc vec![2, 2, 0], // cca ], - ); + ) + .unwrap(); let pad = 3; let prefix = vec![0, 0, 1, 2, 0, 1, 2, 2, 0]; // "aabcabcca" let mut config = prefix.clone(); @@ -209,7 +215,8 @@ fn test_shortestcommonsuperstring_example2_binary() { vec![0, 1, 0], // 010 vec![1, 0, 1], // 101 ], - ); + ) + .unwrap(); let pad = 2; let prefix = vec![0, 0, 1, 1, 0, 1, 0, 0]; // "00110100" let mut config = prefix.clone(); @@ -233,7 +240,8 @@ fn test_shortestcommonsuperstring_example3() { vec![1, 0], // ba vec![1, 1], // bb ], - ); + ) + .unwrap(); let pad = 3; let prefix = vec![0, 1, 2, 0, 1, 1, 0]; // "abcabba" let mut config = prefix.clone(); @@ -246,7 +254,7 @@ fn test_shortestcommonsuperstring_example3() { fn test_shortestcommonsuperstring_paper_example() { // Canonical example_db instance: alphabet {0,1}, strings [0,1] and [1,0]. // Optimal superstring length = 3, witness [0,1,0,pad]. - let problem = ShortestCommonSuperstring::new(2, vec![vec![0, 1], vec![1, 0]]); + let problem = ShortestCommonSuperstring::new(2, vec![vec![0, 1], vec![1, 0]]).unwrap(); assert_eq!( problem .evaluate(&vec![Some(0), Some(1), Some(0), None]) diff --git a/src/unit_tests/models/misc/square_tiling.rs b/src/unit_tests/models/misc/square_tiling.rs index 720a2128a..b26990e1d 100644 --- a/src/unit_tests/models/misc/square_tiling.rs +++ b/src/unit_tests/models/misc/square_tiling.rs @@ -21,8 +21,11 @@ fn test_square_tiling_basic() { assert_eq!(problem.num_tiles(), 4); assert_eq!(problem.grid_size(), 2); assert_eq!(problem.tiles().len(), 4); - assert_eq!(problem.dimensions(), vec![4; 4]); - assert_eq!(problem.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 4] + ); + assert_eq!(problem.num_variables().unwrap(), 4); assert_eq!(::NAME, "SquareTiling"); assert_eq!(::variant(), vec![]); } diff --git a/src/unit_tests/models/misc/stacker_crane.rs b/src/unit_tests/models/misc/stacker_crane.rs index 071b9df8b..2522293d5 100644 --- a/src/unit_tests/models/misc/stacker_crane.rs +++ b/src/unit_tests/models/misc/stacker_crane.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_lengths_and_checks_inferred_vertex_counts() { @@ -40,7 +39,10 @@ fn test_stacker_crane_creation_and_metadata() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_arcs(), 5); assert_eq!(problem.num_edges(), 7); - assert_eq!(problem.dimensions(), vec![5; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 5] + ); assert_eq!(::NAME, "StackerCrane"); assert!(::variant().is_empty()); } diff --git a/src/unit_tests/models/misc/staff_scheduling.rs b/src/unit_tests/models/misc/staff_scheduling.rs index ac58c7aba..b88dbcab4 100644 --- a/src/unit_tests/models/misc/staff_scheduling.rs +++ b/src/unit_tests/models/misc/staff_scheduling.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -29,6 +28,7 @@ fn issue_example_problem() -> StaffScheduling { vec![2, 2, 2, 3, 3, 2, 1], 4, ) + .unwrap() } #[test] @@ -39,29 +39,32 @@ fn test_staff_scheduling_creation() { assert_eq!(problem.num_schedules(), 5); assert_eq!(problem.requirements(), &[2, 2, 2, 3, 3, 2, 1]); assert_eq!(problem.num_workers(), 4); - assert_eq!(problem.dimensions(), vec![5; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 5] + ); } #[test] -#[should_panic(expected = "schedule 1 has 2 periods, expected 3")] fn test_staff_scheduling_new_panics_on_schedule_length_mismatch() { - let _ = StaffScheduling::new( + assert!(StaffScheduling::new( 1, vec![vec![true, false, false], vec![false, true]], vec![1, 1, 1], 2, - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "schedule 1 has 2 active periods, expected 1")] fn test_staff_scheduling_new_panics_on_wrong_active_period_count() { - let _ = StaffScheduling::new( + assert!(StaffScheduling::new( 1, vec![vec![true, false, false], vec![false, true, true]], vec![1, 1, 1], 2, - ); + ) + .is_err()); } #[test] @@ -93,7 +96,7 @@ fn test_staff_scheduling_bruteforce_solver_finds_solution() { #[test] fn test_staff_scheduling_bruteforce_solver_detects_unsat() { let problem = - StaffScheduling::new(1, vec![vec![true, false], vec![false, true]], vec![2, 2], 1); + StaffScheduling::new(1, vec![vec![true, false], vec![false, true]], vec![2, 2], 1).unwrap(); assert!(BruteForce::new().solve(&problem).unwrap().is_none()); } 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 4530eab36..f5e989f9e 100644 --- a/src/unit_tests/models/misc/string_to_string_correction.rs +++ b/src/unit_tests/models/misc/string_to_string_correction.rs @@ -1,11 +1,11 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] fn test_string_to_string_correction_creation() { - let problem = StringToStringCorrection::new(4, vec![0, 1, 2, 3, 1, 0], vec![0, 1, 3, 2, 1], 2); + let problem = + StringToStringCorrection::new(4, vec![0, 1, 2, 3, 1, 0], vec![0, 1, 3, 2, 1], 2).unwrap(); assert_eq!(problem.alphabet_size(), 4); assert_eq!(problem.source(), &[0, 1, 2, 3, 1, 0]); assert_eq!(problem.target(), &[0, 1, 3, 2, 1]); @@ -13,7 +13,10 @@ fn test_string_to_string_correction_creation() { assert_eq!(problem.source_length(), 6); assert_eq!(problem.target_length(), 5); // domain = 2*6+1 = 13, bound = 2 - assert_eq!(problem.dimensions(), vec![13; 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![13; 2] + ); assert_eq!( ::NAME, "StringToStringCorrection" @@ -23,7 +26,8 @@ fn test_string_to_string_correction_creation() { #[test] fn test_string_to_string_correction_evaluation() { - let problem = StringToStringCorrection::new(4, vec![0, 1, 2, 3, 1, 0], vec![0, 1, 3, 2, 1], 2); + let problem = + StringToStringCorrection::new(4, vec![0, 1, 2, 3, 1, 0], vec![0, 1, 3, 2, 1], 2).unwrap(); // Known solution: swap positions 2&3 (value=8), then delete index 5 (value=5) // Step 1: current_len=6, op=8 >= 6, swap_pos = 8-6=2, swap(2,3) → [0,1,3,2,1,0] // Step 2: current_len=6, op=5 < 6, delete(5) → [0,1,3,2,1] = target @@ -34,7 +38,8 @@ fn test_string_to_string_correction_evaluation() { #[test] fn test_string_to_string_correction_invalid_operations() { - let problem = StringToStringCorrection::new(4, vec![0, 1, 2, 3, 1, 0], vec![0, 1, 3, 2, 1], 2); + let problem = + StringToStringCorrection::new(4, vec![0, 1, 2, 3, 1, 0], vec![0, 1, 3, 2, 1], 2).unwrap(); // out-of-domain values assert!(matches!( problem.evaluate(&vec![13, 5]), @@ -58,7 +63,7 @@ fn test_string_to_string_correction_invalid_operations() { #[test] fn test_string_to_string_correction_invalid_after_deletion() { // After a deletion, some swap indices become invalid - let problem = StringToStringCorrection::new(2, vec![0, 1, 0], vec![1], 2); + let problem = StringToStringCorrection::new(2, vec![0, 1, 0], vec![1], 2).unwrap(); // source len = 3, domain = 7, noop = 6 // op=0: delete index 0 → [1, 0], current_len=2 // op=5: 5 >= 2, swap_pos = 5-2=3, need 3+1<2 → false → invalid @@ -67,7 +72,8 @@ fn test_string_to_string_correction_invalid_after_deletion() { #[test] fn test_string_to_string_correction_serialization() { - let problem = StringToStringCorrection::new(4, vec![0, 1, 2, 3, 1, 0], vec![0, 1, 3, 2, 1], 2); + let problem = + StringToStringCorrection::new(4, vec![0, 1, 2, 3, 1, 0], vec![0, 1, 3, 2, 1], 2).unwrap(); let json = serde_json::to_value(&problem).unwrap(); let restored: StringToStringCorrection = serde_json::from_value(json).unwrap(); assert_eq!(restored.alphabet_size(), problem.alphabet_size()); @@ -80,7 +86,7 @@ fn test_string_to_string_correction_serialization() { fn test_string_to_string_correction_solver() { // Small instance: source [0,1], target [1,0], bound 1 // Need a single swap: swap_pos=0, value = current_len + 0 = 2 - let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1, 0], 1); + let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1, 0], 1).unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -92,7 +98,8 @@ fn test_string_to_string_correction_solver() { #[test] fn test_string_to_string_correction_paper_example() { // Paper example: 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 problem = + StringToStringCorrection::new(4, vec![0, 1, 2, 3, 1, 0], vec![0, 1, 3, 2, 1], 2).unwrap(); // Verify the known solution assert!(problem.evaluate(&vec![8, 5]).unwrap()); @@ -110,8 +117,11 @@ fn test_string_to_string_correction_paper_example() { #[test] fn test_string_to_string_correction_unsatisfiable() { // bound=0, source != target → impossible - let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1, 0], 0); - assert_eq!(problem.dimensions(), Vec::::new()); + let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1, 0], 0).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(!problem.evaluate(&vec![]).unwrap()); let solver = BruteForce::new(); @@ -121,14 +131,14 @@ fn test_string_to_string_correction_unsatisfiable() { #[test] fn test_string_to_string_correction_identity() { // source == target, bound_k=0 → satisfied with empty config - let problem = StringToStringCorrection::new(2, vec![0, 1], vec![0, 1], 0); + let problem = StringToStringCorrection::new(2, vec![0, 1], vec![0, 1], 0).unwrap(); assert!(problem.evaluate(&vec![]).unwrap()); } #[test] fn test_string_to_string_correction_empty_strings() { // Both empty, bound_k=0 → trivially satisfied - let problem = StringToStringCorrection::new(0, vec![], vec![], 0); + let problem = StringToStringCorrection::new(0, vec![], vec![], 0).unwrap(); assert!(problem.evaluate(&vec![]).unwrap()); } @@ -136,7 +146,7 @@ fn test_string_to_string_correction_empty_strings() { fn test_string_to_string_correction_delete_only() { // source [0,1,2], target [0,2], bound 1 // Delete index 1: op=1, current_len=3, 1<3 → delete → [0,2] = target - let problem = StringToStringCorrection::new(3, vec![0, 1, 2], vec![0, 2], 1); + let problem = StringToStringCorrection::new(3, vec![0, 1, 2], vec![0, 2], 1).unwrap(); assert!(problem.evaluate(&vec![1]).unwrap()); let solver = BruteForce::new(); @@ -149,19 +159,19 @@ fn test_string_to_string_correction_delete_only() { #[test] fn test_string_to_string_correction_rejects_target_longer_than_source() { - let problem = StringToStringCorrection::new(3, vec![0, 1], vec![0, 1, 2], 1); + let problem = StringToStringCorrection::new(3, vec![0, 1], vec![0, 1, 2], 1).unwrap(); assert!(!problem.evaluate(&vec![4]).unwrap()); } #[test] fn test_string_to_string_correction_rejects_excessive_deletions_requirement() { - let problem = StringToStringCorrection::new(4, vec![0, 1, 2, 3], vec![0], 2); + let problem = StringToStringCorrection::new(4, vec![0, 1, 2, 3], vec![0], 2).unwrap(); assert!(!problem.evaluate(&vec![8, 8]).unwrap()); } #[test] fn test_string_to_string_correction_is_available_in_prelude() { - let problem = crate::prelude::StringToStringCorrection::new(2, vec![0], vec![0], 0); + let problem = crate::prelude::StringToStringCorrection::new(2, vec![0], vec![0], 0).unwrap(); assert!(problem.evaluate(&vec![]).unwrap()); } diff --git a/src/unit_tests/models/misc/subset_product.rs b/src/unit_tests/models/misc/subset_product.rs index 25c767a06..08349106f 100644 --- a/src/unit_tests/models/misc/subset_product.rs +++ b/src/unit_tests/models/misc/subset_product.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use num_bigint::BigUint; @@ -14,18 +13,21 @@ fn buv(values: &[u32]) -> Vec { #[test] fn test_subsetproduct_basic() { - let problem = SubsetProduct::new(vec![2u32, 3, 5, 7, 6, 10], 210u32); + let problem = SubsetProduct::new(vec![2u32, 3, 5, 7, 6, 10], 210u32).unwrap(); assert_eq!(problem.num_elements(), 6); assert_eq!(problem.sizes(), buv(&[2, 3, 5, 7, 6, 10]).as_slice()); assert_eq!(problem.target(), &bu(210)); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!(::NAME, "SubsetProduct"); assert_eq!(::variant(), vec![]); } #[test] fn test_subsetproduct_evaluate_satisfying() { - let problem = SubsetProduct::new(vec![2u32, 3, 5, 7, 6, 10], 210u32); + let problem = SubsetProduct::new(vec![2u32, 3, 5, 7, 6, 10], 210u32).unwrap(); // {2, 3, 5, 7} = 210 assert!(problem .evaluate(&vec![true, true, true, true, false, false]) @@ -38,7 +40,7 @@ fn test_subsetproduct_evaluate_satisfying() { #[test] fn test_subsetproduct_evaluate_unsatisfying() { - let problem = SubsetProduct::new(vec![2u32, 3, 5, 7, 6, 10], 210u32); + let problem = SubsetProduct::new(vec![2u32, 3, 5, 7, 6, 10], 210u32).unwrap(); // {2, 3} = 6 != 210 assert!(!problem .evaluate(&vec![true, true, false, false, false, false]) @@ -55,7 +57,7 @@ fn test_subsetproduct_evaluate_unsatisfying() { #[test] fn test_subsetproduct_evaluate_wrong_config_length() { - let problem = SubsetProduct::new(vec![2u32, 3, 5], 30u32); + let problem = SubsetProduct::new(vec![2u32, 3, 5], 30u32).unwrap(); assert!(matches!( problem.evaluate(&vec![true, false]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -68,7 +70,7 @@ fn test_subsetproduct_evaluate_wrong_config_length() { #[test] fn test_subsetproduct_evaluate_invalid_variable_value() { - let problem = SubsetProduct::new(vec![2u32, 3], 6u32); + let problem = SubsetProduct::new(vec![2u32, 3], 6u32).unwrap(); assert!( crate::registry::DynProblem::evaluate_dyn(&problem, &serde_json::json!([2, false])) .is_err() @@ -80,7 +82,10 @@ fn test_subsetproduct_empty_instance() { // Empty set, target 1: empty subset product = 1 satisfies let problem = SubsetProduct::new_unchecked(vec![], bu(1)); assert_eq!(problem.num_elements(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } @@ -93,7 +98,7 @@ fn test_subsetproduct_empty_instance_nonunit_target() { #[test] fn test_subsetproduct_brute_force() { - let problem = SubsetProduct::new(vec![2u32, 3, 5, 7, 6, 10], 210u32); + let problem = SubsetProduct::new(vec![2u32, 3, 5, 7, 6, 10], 210u32).unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -104,7 +109,7 @@ fn test_subsetproduct_brute_force() { #[test] fn test_subsetproduct_brute_force_all() { - let problem = SubsetProduct::new(vec![2u32, 3, 5, 7, 6, 10], 210u32); + let problem = SubsetProduct::new(vec![2u32, 3, 5, 7, 6, 10], 210u32).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -116,7 +121,7 @@ fn test_subsetproduct_brute_force_all() { #[test] fn test_subsetproduct_unsatisfiable() { // Target 1000 is unreachable with these sizes - let problem = SubsetProduct::new(vec![2u32, 3, 5], 1000u32); + let problem = SubsetProduct::new(vec![2u32, 3, 5], 1000u32).unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); assert!(solution.is_none()); @@ -124,7 +129,7 @@ fn test_subsetproduct_unsatisfiable() { #[test] fn test_subsetproduct_serialization() { - let problem = SubsetProduct::new(vec![2u32, 3, 5, 7, 6, 10], 210u32); + let problem = SubsetProduct::new(vec![2u32, 3, 5, 7, 6, 10], 210u32).unwrap(); let json = serde_json::to_value(&problem).unwrap(); assert_eq!( json, @@ -149,7 +154,7 @@ fn test_subsetproduct_deserialization_rejects_numeric_json() { #[test] fn test_subsetproduct_single_element() { - let problem = SubsetProduct::new(vec![5u32], 5u32); + let problem = SubsetProduct::new(vec![5u32], 5u32).unwrap(); assert!(problem.evaluate(&vec![true]).unwrap()); assert!(!problem.evaluate(&vec![false]).unwrap()); } @@ -157,39 +162,36 @@ fn test_subsetproduct_single_element() { #[test] fn test_subsetproduct_all_selected() { // Target equals product of all elements - let problem = SubsetProduct::new(vec![2u32, 3, 5], 30u32); + let problem = SubsetProduct::new(vec![2u32, 3, 5], 30u32).unwrap(); assert!(problem.evaluate(&vec![true, true, true]).unwrap()); // 2*3*5 = 30 } #[test] fn test_subsetproduct_target_one() { // Target 1 with non-empty set: only empty subset works (product = 1) - let problem = SubsetProduct::new(vec![2u32, 3, 5], 1u32); + let problem = SubsetProduct::new(vec![2u32, 3, 5], 1u32).unwrap(); assert!(problem.evaluate(&vec![false, false, false]).unwrap()); // empty subset product = 1 assert!(!problem.evaluate(&vec![true, false, false]).unwrap()); // 2 != 1 } #[test] -#[should_panic(expected = "positive")] -fn test_subsetproduct_negative_sizes_panic() { - SubsetProduct::new(vec![-1i64, 2, 3], 4u32); +fn test_subsetproduct_negative_sizes_is_rejected() { + assert!(SubsetProduct::new(vec![-1i64, 2, 3], 4u32).is_err()); } #[test] -#[should_panic(expected = "positive")] -fn test_subsetproduct_zero_size_panic() { - SubsetProduct::new(vec![0i64, 2, 3], 4u32); +fn test_subsetproduct_zero_size_is_rejected() { + assert!(SubsetProduct::new(vec![0i64, 2, 3], 4u32).is_err()); } #[test] -#[should_panic(expected = "positive")] -fn test_subsetproduct_zero_target_panic() { - SubsetProduct::new(vec![2u32, 3], 0u32); +fn test_subsetproduct_zero_target_is_rejected() { + assert!(SubsetProduct::new(vec![2u32, 3], 0u32).is_err()); } #[test] fn test_subsetproduct_large_integer_input() { - let problem = SubsetProduct::new(vec![2i128, 3, 5, 7, 6, 10], 210i128); + let problem = SubsetProduct::new(vec![2i128, 3, 5, 7, 6, 10], 210i128).unwrap(); assert!(problem .evaluate(&vec![true, true, true, true, false, false]) .unwrap()); // 2*3*5*7 = 210 diff --git a/src/unit_tests/models/misc/subset_sum.rs b/src/unit_tests/models/misc/subset_sum.rs index 80807cefe..ed2bfb6fc 100644 --- a/src/unit_tests/models/misc/subset_sum.rs +++ b/src/unit_tests/models/misc/subset_sum.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use num_bigint::BigUint; @@ -14,18 +13,21 @@ fn buv(values: &[u32]) -> Vec { #[test] fn test_subsetsum_basic() { - let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); + let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32).unwrap(); assert_eq!(problem.num_elements(), 6); assert_eq!(problem.sizes(), buv(&[3, 7, 1, 8, 2, 4]).as_slice()); assert_eq!(problem.target(), &bu(11)); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!(::NAME, "SubsetSum"); assert_eq!(::variant(), vec![]); } #[test] fn test_subsetsum_evaluate_satisfying() { - let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); + let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32).unwrap(); // {3, 8} = 11 assert!(problem .evaluate(&vec![true, false, false, true, false, false]) @@ -38,7 +40,7 @@ fn test_subsetsum_evaluate_satisfying() { #[test] fn test_subsetsum_evaluate_unsatisfying() { - let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); + let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32).unwrap(); // {3, 7} = 10 ≠ 11 assert!(!problem .evaluate(&vec![true, true, false, false, false, false]) @@ -55,7 +57,7 @@ fn test_subsetsum_evaluate_unsatisfying() { #[test] fn test_subsetsum_evaluate_wrong_config_length() { - let problem = SubsetSum::new(vec![3u32, 7, 1], 10u32); + let problem = SubsetSum::new(vec![3u32, 7, 1], 10u32).unwrap(); assert!(matches!( problem.evaluate(&vec![true, false]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -68,7 +70,7 @@ fn test_subsetsum_evaluate_wrong_config_length() { #[test] fn test_subsetsum_evaluate_invalid_variable_value() { - let problem = SubsetSum::new(vec![3u32, 7], 10u32); + let problem = SubsetSum::new(vec![3u32, 7], 10u32).unwrap(); assert!( crate::registry::DynProblem::evaluate_dyn(&problem, &serde_json::json!([2, false])) .is_err() @@ -80,7 +82,10 @@ fn test_subsetsum_empty_instance() { // Empty set, target 0: empty subset satisfies let problem = SubsetSum::new_unchecked(vec![], bu(0)); assert_eq!(problem.num_elements(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } @@ -93,7 +98,7 @@ fn test_subsetsum_empty_instance_nonzero_target() { #[test] fn test_subsetsum_brute_force() { - let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); + let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32).unwrap(); let solver = BruteForce::new(); let solution = solver .solve(&problem) @@ -104,7 +109,7 @@ fn test_subsetsum_brute_force() { #[test] fn test_subsetsum_brute_force_all() { - let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); + let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -116,7 +121,7 @@ fn test_subsetsum_brute_force_all() { #[test] fn test_subsetsum_unsatisfiable() { // Target 100 is unreachable - let problem = SubsetSum::new(vec![1u32, 2, 3], 100u32); + let problem = SubsetSum::new(vec![1u32, 2, 3], 100u32).unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); assert!(solution.is_none()); @@ -124,7 +129,7 @@ fn test_subsetsum_unsatisfiable() { #[test] fn test_subsetsum_serialization() { - let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); + let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32).unwrap(); let json = serde_json::to_value(&problem).unwrap(); assert_eq!( json, @@ -149,7 +154,7 @@ fn test_subsetsum_deserialization_rejects_numeric_json() { #[test] fn test_subsetsum_single_element() { - let problem = SubsetSum::new(vec![5u32], 5u32); + let problem = SubsetSum::new(vec![5u32], 5u32).unwrap(); assert!(problem.evaluate(&vec![true]).unwrap()); assert!(!problem.evaluate(&vec![false]).unwrap()); } @@ -157,7 +162,7 @@ fn test_subsetsum_single_element() { #[test] fn test_subsetsum_all_selected() { // Target equals sum of all elements - let problem = SubsetSum::new(vec![1u32, 2, 3, 4], 10u32); + let problem = SubsetSum::new(vec![1u32, 2, 3, 4], 10u32).unwrap(); assert!(problem.evaluate(&vec![true, true, true, true]).unwrap()); // 1+2+3+4 = 10 } @@ -170,20 +175,18 @@ fn test_subsetsum_target_zero() { } #[test] -#[should_panic(expected = "positive")] -fn test_subsetsum_negative_sizes_panic() { - SubsetSum::new(vec![-1i64, 2, 3], 4u32); +fn test_subsetsum_negative_sizes_is_rejected() { + assert!(SubsetSum::new(vec![-1i64, 2, 3], 4u32).is_err()); } #[test] -#[should_panic(expected = "positive")] -fn test_subsetsum_zero_size_panic() { - SubsetSum::new(vec![0i64, 2, 3], 4u32); +fn test_subsetsum_zero_size_is_rejected() { + assert!(SubsetSum::new(vec![0i64, 2, 3], 4u32).is_err()); } #[test] fn test_subsetsum_large_integer_input() { - let problem = SubsetSum::new(vec![3i128, 7, 1, 8, 2, 4], 11i128); + let problem = SubsetSum::new(vec![3i128, 7, 1, 8, 2, 4], 11i128).unwrap(); assert!(problem .evaluate(&vec![true, false, false, true, false, false]) .unwrap()); // 3 + 8 = 11 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 5afb1a20e..4201e33d3 100644 --- a/src/unit_tests/models/misc/sum_of_squares_partition.rs +++ b/src/unit_tests/models/misc/sum_of_squares_partition.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -10,7 +9,10 @@ fn test_sum_of_squares_partition_basic() { assert_eq!(problem.num_elements(), 6); assert_eq!(problem.num_groups(), 3); assert_eq!(problem.sizes(), &[5, 3, 8, 2, 7, 1]); - assert_eq!(problem.dimensions(), vec![3; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 6] + ); assert_eq!( ::NAME, "SumOfSquaresPartition" diff --git a/src/unit_tests/models/misc/three_partition.rs b/src/unit_tests/models/misc/three_partition.rs index 44c354c02..857e61430 100644 --- a/src/unit_tests/models/misc/three_partition.rs +++ b/src/unit_tests/models/misc/three_partition.rs @@ -16,8 +16,11 @@ fn test_three_partition_basic() { assert_eq!(problem.num_elements(), 6); assert_eq!(problem.num_groups(), 2); assert_eq!(problem.total_sum(), 30); - assert_eq!(problem.dimensions(), vec![2; 6]); - assert_eq!(problem.num_variables(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); + assert_eq!(problem.num_variables().unwrap(), 6); assert_eq!(::NAME, "ThreePartition"); assert_eq!(::variant(), vec![]); } diff --git a/src/unit_tests/models/misc/timetable_design.rs b/src/unit_tests/models/misc/timetable_design.rs index ee8aaad02..7df1dd3a7 100644 --- a/src/unit_tests/models/misc/timetable_design.rs +++ b/src/unit_tests/models/misc/timetable_design.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_matrix_shape_mismatch() { @@ -34,6 +33,7 @@ fn timetable_design_toy_problem() -> TimetableDesign { vec![vec![true, true], vec![false, true]], vec![vec![1, 0], vec![0, 1]], ) + .unwrap() } #[test] @@ -49,7 +49,10 @@ fn test_timetable_design_creation_and_dims() { ); assert_eq!(problem.task_avail(), &[vec![true, true], vec![false, true]]); assert_eq!(problem.requirements(), &[vec![1, 0], vec![0, 1]]); - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); } #[test] @@ -59,29 +62,29 @@ fn test_timetable_design_problem_name_and_variant() { } #[test] -#[should_panic(expected = "craftsman_avail has 1 rows, expected 2")] fn test_timetable_design_new_panics_on_craftsman_row_count_mismatch() { - let _ = TimetableDesign::new( + assert!(TimetableDesign::new( 2, 2, 2, vec![vec![true, false]], vec![vec![true, true], vec![false, true]], vec![vec![1, 0], vec![0, 1]], - ); + ) + .is_err()); } #[test] -#[should_panic(expected = "requirements row 0 has 1 tasks, expected 2")] fn test_timetable_design_new_panics_on_requirement_width_mismatch() { - let _ = TimetableDesign::new( + assert!(TimetableDesign::new( 2, 2, 2, vec![vec![true, false], vec![true, true]], vec![vec![true, true], vec![false, true]], vec![vec![1], vec![0, 1]], - ); + ) + .is_err()); } #[test] @@ -160,7 +163,8 @@ fn test_timetable_design_customized_solver_returns_none_for_infeasible_instance( vec![vec![true], vec![true]], vec![vec![true]], vec![vec![1], vec![1]], - ); + ) + .unwrap(); assert!(problem.solve_via_required_assignments().is_none()); } diff --git a/src/unit_tests/models/set/comparative_containment.rs b/src/unit_tests/models/set/comparative_containment.rs index 8d0ecd984..2f58e7085 100644 --- a/src/unit_tests/models/set/comparative_containment.rs +++ b/src/unit_tests/models/set/comparative_containment.rs @@ -55,8 +55,11 @@ fn test_comparative_containment_creation() { assert_eq!(problem.universe_size(), 4); assert_eq!(problem.num_r_sets(), 2); assert_eq!(problem.num_s_sets(), 2); - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); } #[test] diff --git a/src/unit_tests/models/set/consecutive_sets.rs b/src/unit_tests/models/set/consecutive_sets.rs index 4c94c0afa..f2546c8f8 100644 --- a/src/unit_tests/models/set/consecutive_sets.rs +++ b/src/unit_tests/models/set/consecutive_sets.rs @@ -9,12 +9,16 @@ fn test_consecutive_sets_creation() { 6, vec![vec![0, 4], vec![2, 4], vec![2, 5], vec![1, 5], vec![1, 3]], 6, - ); + ) + .unwrap(); assert_eq!(problem.alphabet_size(), 6); assert_eq!(problem.num_subsets(), 5); assert_eq!(problem.bound_k(), 6); - assert_eq!(problem.num_variables(), 6); - assert_eq!(problem.dimensions(), vec![7; 6]); // alphabet_size + 1 = 7 + assert_eq!(problem.num_variables().unwrap(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![7; 6] + ); // alphabet_size + 1 = 7 } #[test] @@ -23,7 +27,8 @@ fn test_consecutive_sets_evaluation() { 6, vec![vec![0, 4], vec![2, 4], vec![2, 5], vec![1, 5], vec![1, 3]], 6, - ); + ) + .unwrap(); // YES: w = [0, 4, 2, 5, 1, 3] assert!(problem .evaluate(&vec![Some(0), Some(4), Some(2), Some(5), Some(1), Some(3)]) @@ -42,7 +47,7 @@ fn test_consecutive_sets_no_instance() { // In any string of length <= 3 over {0,1,2}, we cannot have all three pairs adjacent. // E.g., [0,1,2] satisfies {0,1} and {1,2} but not {0,2}. // 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 problem = ConsecutiveSets::new(3, vec![vec![0, 1], vec![1, 2], vec![0, 2]], 3).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(solutions.is_empty()); @@ -52,7 +57,7 @@ fn test_consecutive_sets_no_instance() { fn test_consecutive_sets_solver() { // Small YES instance: alphabet_size=3, subsets=[{0,1},{1,2}], bound_k=3 // 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 problem = ConsecutiveSets::new(3, vec![vec![0, 1], vec![1, 2]], 3).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -65,7 +70,7 @@ fn test_consecutive_sets_solver() { #[test] fn test_consecutive_sets_rejects_wrong_config_length() { - let problem = ConsecutiveSets::new(3, vec![vec![0, 1]], 3); + let problem = ConsecutiveSets::new(3, vec![vec![0, 1]], 3).unwrap(); assert!(matches!( problem.evaluate(&vec![Some(0), Some(1)]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -79,7 +84,7 @@ fn test_consecutive_sets_rejects_wrong_config_length() { #[test] fn test_consecutive_sets_rejects_internal_unused() { // Internal "unused" symbol should be rejected - let problem = ConsecutiveSets::new(3, vec![vec![0, 1]], 4); + let problem = ConsecutiveSets::new(3, vec![vec![0, 1]], 4).unwrap(); // [0, 3, 1, 3] has "unused" (3) at position 1, which is internal assert!(!problem .evaluate(&vec![Some(0), None, Some(1), None]) @@ -88,7 +93,7 @@ fn test_consecutive_sets_rejects_internal_unused() { #[test] fn test_consecutive_sets_accepts_shorter_string_with_trailing_unused() { - let problem = ConsecutiveSets::new(3, vec![vec![0, 1]], 4); + let problem = ConsecutiveSets::new(3, vec![vec![0, 1]], 4).unwrap(); assert!(problem .evaluate(&vec![Some(0), Some(1), None, None]) .unwrap()); @@ -96,13 +101,13 @@ fn test_consecutive_sets_accepts_shorter_string_with_trailing_unused() { #[test] fn test_consecutive_sets_rejects_duplicate_window_symbol() { - let problem = ConsecutiveSets::new(2, vec![vec![0, 1]], 2); + let problem = ConsecutiveSets::new(2, vec![vec![0, 1]], 2).unwrap(); assert!(!problem.evaluate(&vec![Some(0), Some(0)]).unwrap()); } #[test] fn test_consecutive_sets_serialization() { - let problem = ConsecutiveSets::new(6, vec![vec![0, 4], vec![2, 4]], 6); + let problem = ConsecutiveSets::new(6, vec![vec![0, 4], vec![2, 4]], 6).unwrap(); let json = serde_json::to_string(&problem).unwrap(); let deserialized: ConsecutiveSets = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.alphabet_size(), problem.alphabet_size()); @@ -114,7 +119,7 @@ fn test_consecutive_sets_serialization() { #[test] fn test_consecutive_sets_empty_subsets() { // Empty collection — trivially satisfiable by any string (even empty) - let problem = ConsecutiveSets::new(3, vec![], 3); + let problem = ConsecutiveSets::new(3, vec![], 3).unwrap(); // All unused = empty string is fine assert!(problem.evaluate(&vec![None; 3]).unwrap()); let solver = BruteForce::new(); @@ -123,19 +128,23 @@ fn test_consecutive_sets_empty_subsets() { } #[test] -#[should_panic(expected = "outside alphabet")] fn test_consecutive_sets_element_out_of_range() { - ConsecutiveSets::new(3, vec![vec![0, 5]], 3); + assert!(ConsecutiveSets::new(3, vec![vec![0, 5]], 3).is_err()); } #[test] -#[should_panic(expected = "duplicate elements")] fn test_consecutive_sets_duplicate_elements() { - ConsecutiveSets::new(3, vec![vec![1, 1]], 3); + assert!(ConsecutiveSets::new(3, vec![vec![1, 1]], 3).is_err()); } #[test] -#[should_panic(expected = "bound_k must be positive")] fn test_consecutive_sets_zero_bound() { - ConsecutiveSets::new(3, vec![vec![0, 1]], 0); + assert!(ConsecutiveSets::new(3, vec![vec![0, 1]], 0).is_err()); +} + +#[test] +fn json_rejects_invalid_instance() { + let json = serde_json::json!({"alphabet_size":3,"subsets":[[0,0]],"bound_k":3}); + assert!(serde_json::from_value::(json.clone()).is_err()); + assert!(crate::registry::load_dyn("ConsecutiveSets", &Default::default(), json).is_err()); } 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 a7cdf05cc..18dbe7a05 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 @@ -14,19 +14,22 @@ use crate::traits::Problem; #[test] fn test_exact_cover_by_3_sets_creation() { - let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]); + let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]).unwrap(); assert_eq!(problem.universe_size(), 6); assert_eq!(problem.num_subsets(), 3); assert_eq!(problem.num_sets(), 3); - assert_eq!(problem.num_variables(), 3); - assert_eq!(problem.dimensions(), vec![2, 2, 2]); + assert_eq!(problem.num_variables().unwrap(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2] + ); } #[test] fn test_exact_cover_by_3_sets_evaluation() { // Universe: {0,1,2,3,4,5}, q=2 // S0={0,1,2}, S1={3,4,5}, S2={0,3,4} - let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]); + let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]).unwrap(); // S0 + S1 = exact cover assert!(problem.evaluate(&vec![true, true, false]).unwrap()); @@ -46,7 +49,7 @@ fn test_exact_cover_by_3_sets_evaluation() { #[test] fn test_exact_cover_by_3_sets_rejects_wrong_config_length() { - let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]); + let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]).unwrap(); assert!(matches!( problem.evaluate(&vec![true, true, false]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -55,7 +58,7 @@ fn test_exact_cover_by_3_sets_rejects_wrong_config_length() { #[test] fn test_exact_cover_by_3_sets_rejects_non_binary_config_values() { - let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]); + let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]).unwrap(); assert!(crate::registry::DynProblem::evaluate_dyn( &problem, &serde_json::json!([true, true, 2]) @@ -79,7 +82,8 @@ fn test_exact_cover_by_3_sets_solver() { [1, 4, 6], [2, 5, 8], ], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -98,7 +102,7 @@ fn test_exact_cover_by_3_sets_no_solution() { // Universe: {0,1,2,3,4,5}, q=2 // All subsets overlap: S0={0,1,2}, S1={0,3,4}, S2={0,4,5} // Every pair shares element 0, so no exact cover exists - let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [0, 3, 4], [0, 4, 5]]); + let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [0, 3, 4], [0, 4, 5]]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -107,7 +111,7 @@ fn test_exact_cover_by_3_sets_no_solution() { #[test] fn test_exact_cover_by_3_sets_serialization() { - let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]); + let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]).unwrap(); let json = serde_json::to_string(&problem).unwrap(); let deserialized: ExactCoverBy3Sets = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.universe_size(), problem.universe_size()); @@ -119,14 +123,14 @@ fn test_exact_cover_by_3_sets_serialization() { #[test] fn test_exact_cover_by_3_sets_is_valid_solution() { - let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]); + let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]).unwrap(); assert!(problem.is_valid_solution(&[true, true]).unwrap()); assert!(!problem.is_valid_solution(&[true, false]).unwrap()); } #[test] fn test_exact_cover_by_3_sets_covered_elements() { - let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]); + let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]).unwrap(); let covered = problem.covered_elements(&[true, false, true]); assert_eq!(covered.len(), 5); // {0,1,2,3,4} -- note element 0 appears twice assert!(covered.contains(&0)); @@ -136,7 +140,7 @@ fn test_exact_cover_by_3_sets_covered_elements() { #[test] fn test_exact_cover_by_3_sets_get_subset() { - let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]); + let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]).unwrap(); assert_eq!(problem.get_subset(0), Some(&[0, 1, 2])); assert_eq!(problem.get_subset(1), Some(&[3, 4, 5])); assert_eq!(problem.get_subset(2), None); @@ -145,7 +149,7 @@ fn test_exact_cover_by_3_sets_get_subset() { #[test] fn test_exact_cover_by_3_sets_empty() { // Empty universe with no subsets -- trivially satisfiable - let problem = ExactCoverBy3Sets::new(0, vec![]); + let problem = ExactCoverBy3Sets::new(0, vec![]).unwrap(); assert!(problem.evaluate(&vec![]).unwrap()); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -153,19 +157,26 @@ fn test_exact_cover_by_3_sets_empty() { } #[test] -#[should_panic(expected = "Universe size must be divisible by 3")] -fn test_exact_cover_by_3_sets_invalid_universe_size() { - ExactCoverBy3Sets::new(5, vec![[0, 1, 2]]); -} - -#[test] -#[should_panic(expected = "outside universe")] -fn test_exact_cover_by_3_sets_element_out_of_range() { - ExactCoverBy3Sets::new(6, vec![[0, 1, 7]]); +fn construction_and_json_reject_invalid_triples() { + for (universe_size, subsets) in [ + (5, vec![[0, 1, 2]]), + (6, vec![[0, 1, 7]]), + (6, vec![[0, 0, 1]]), + ] { + assert!(ExactCoverBy3Sets::new(universe_size, subsets.clone()).is_err()); + let json = serde_json::json!({"universe_size": universe_size, "subsets": subsets}); + assert!(serde_json::from_value::(json.clone()).is_err()); + assert!(crate::registry::load_dyn("ExactCoverBy3Sets", &Default::default(), json).is_err()); + } } #[test] -#[should_panic(expected = "contains duplicate elements")] -fn test_exact_cover_by_3_sets_duplicate_elements() { - ExactCoverBy3Sets::new(6, vec![[0, 0, 1]]); +fn construction_and_json_sort_triples() { + let expected = ExactCoverBy3Sets::new(3, vec![[2, 0, 1]]).unwrap(); + let loaded: ExactCoverBy3Sets = serde_json::from_value(serde_json::json!({ + "universe_size": 3, "subsets": [[2, 0, 1]] + })) + .unwrap(); + assert_eq!(expected.subsets(), &[[0, 1, 2]]); + assert_eq!(loaded.subsets(), expected.subsets()); } diff --git a/src/unit_tests/models/set/integer_knapsack.rs b/src/unit_tests/models/set/integer_knapsack.rs index b03fc1744..e767866ae 100644 --- a/src/unit_tests/models/set/integer_knapsack.rs +++ b/src/unit_tests/models/set/integer_knapsack.rs @@ -10,7 +10,10 @@ fn test_integer_knapsack_basic() { assert_eq!(problem.values(), &[4, 5, 7, 3, 9]); assert_eq!(problem.capacity(), 15); // dims: floor(15/3)+1=6, floor(15/4)+1=4, floor(15/5)+1=4, floor(15/2)+1=8, floor(15/7)+1=3 - assert_eq!(problem.dimensions(), vec![6, 4, 4, 8, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6, 4, 4, 8, 3] + ); assert_eq!(::NAME, "IntegerKnapsack"); assert_eq!(::variant(), vec![]); } @@ -65,20 +68,19 @@ fn test_integer_knapsack_evaluate_wrong_config_length() { } #[test] -fn test_integer_knapsack_evaluate_out_of_domain() { +fn test_integer_knapsack_evaluate_single_item_overweight() { let problem = IntegerKnapsack::new(vec![3, 4], vec![4, 5], 10).unwrap(); - // dims = [4, 3], so config [4, 0] is out of domain for item 0 - assert!(matches!( - problem.evaluate(&vec![4, 0]), - Err(crate::traits::EvaluationError::InvalidConfiguration(_)) - )); + assert_eq!(problem.evaluate(&vec![4, 0]).unwrap(), Max(None)); } #[test] fn test_integer_knapsack_empty_instance() { let problem = IntegerKnapsack::new(vec![], vec![], 10).unwrap(); assert_eq!(problem.num_items(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), Max(Some(0))); } @@ -107,7 +109,10 @@ fn test_integer_knapsack_serialization() { #[test] fn test_integer_knapsack_zero_capacity() { let problem = IntegerKnapsack::new(vec![1, 2], vec![10, 20], 0).unwrap(); - assert_eq!(problem.dimensions(), vec![1, 1]); // floor(0/1)+1=1, floor(0/2)+1=1 + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1, 1] + ); // floor(0/1)+1=1, floor(0/2)+1=1 assert_eq!(problem.evaluate(&vec![0, 0]).unwrap(), Max(Some(0))); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap().unwrap(); @@ -118,7 +123,10 @@ fn test_integer_knapsack_zero_capacity() { #[test] fn test_integer_knapsack_dimension_uses_structural_range() { let problem = IntegerKnapsack::new(vec![1], vec![1], i64::MAX).unwrap(); - assert_eq!(problem.dimensions(), vec![1_usize << 63]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1_usize << 63] + ); } #[test] @@ -126,7 +134,10 @@ fn test_integer_knapsack_single_item() { // Single item size=3, value=5, capacity=7 // Max multiplicity: floor(7/3)=2, dims=[3] let problem = IntegerKnapsack::new(vec![3], vec![5], 7).unwrap(); - assert_eq!(problem.dimensions(), vec![3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3] + ); assert_eq!(problem.evaluate(&vec![0]).unwrap(), Max(Some(0))); assert_eq!(problem.evaluate(&vec![1]).unwrap(), Max(Some(5))); assert_eq!(problem.evaluate(&vec![2]).unwrap(), Max(Some(10))); @@ -231,7 +242,7 @@ fn test_integer_knapsack_deserialization_rejects_invalid_fields() { #[test] fn test_integer_knapsack_paper_example() { - // From issue #532: 5 items, sizes=[3,4,5,2,7], values=[4,5,7,3,9], B=15 + // 5 items, sizes=[3,4,5,2,7], values=[4,5,7,3,9], B=15 // Optimal=22 with c=(0,0,1,5,0) or c=(1,0,0,6,0) let problem = IntegerKnapsack::new(vec![3, 4, 5, 2, 7], vec![4, 5, 7, 3, 9], 15).unwrap(); diff --git a/src/unit_tests/models/set/maximum_set_packing.rs b/src/unit_tests/models/set/maximum_set_packing.rs index 9993ca2ef..581de7e13 100644 --- a/src/unit_tests/models/set/maximum_set_packing.rs +++ b/src/unit_tests/models/set/maximum_set_packing.rs @@ -1,9 +1,9 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Max; -include!("../../jl_helpers.rs"); #[test] fn test_maximum_set_packing_create_spec_uses_subsets_input() { @@ -24,7 +24,7 @@ fn test_maximum_set_packing_create_spec_uses_subsets_input() { fn test_set_packing_creation() { let problem = MaximumSetPacking::::new(vec![vec![0, 1], vec![1, 2], vec![3, 4]]); assert_eq!(problem.num_sets(), 3); - assert_eq!(problem.num_variables(), 3); + assert_eq!(problem.num_variables().unwrap(), 3); } #[test] @@ -90,7 +90,8 @@ fn test_relationship_to_independent_set() { // Build intersection graph let edges = sp_problem.overlapping_pairs(); let n = sets.len(); - let is_problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges), vec![1i64; n]); + let is_problem = + MaximumIndependentSet::new(SimpleGraph::new(n, edges).unwrap(), vec![1i64; n]).unwrap(); let solver = BruteForce::new(); diff --git a/src/unit_tests/models/set/minimum_cardinality_key.rs b/src/unit_tests/models/set/minimum_cardinality_key.rs index d73369de6..9a49272a7 100644 --- a/src/unit_tests/models/set/minimum_cardinality_key.rs +++ b/src/unit_tests/models/set/minimum_cardinality_key.rs @@ -16,12 +16,13 @@ fn instance1() -> MinimumCardinalityKey { (vec![2, 4], vec![5]), ], ) + .unwrap() } /// Instance 2 from the issue: 6 attributes, FDs {0,1,2}->{3}, {3,4}->{5}. /// No 2-element subset determines all attributes. fn instance2() -> MinimumCardinalityKey { - MinimumCardinalityKey::new(6, vec![(vec![0, 1, 2], vec![3]), (vec![3, 4], vec![5])]) + MinimumCardinalityKey::new(6, vec![(vec![0, 1, 2], vec![3]), (vec![3, 4], vec![5])]).unwrap() } #[test] @@ -29,8 +30,11 @@ fn test_minimum_cardinality_key_creation() { let problem = instance1(); assert_eq!(problem.num_attributes(), 6); assert_eq!(problem.num_dependencies(), 4); - assert_eq!(problem.num_variables(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!(problem.num_variables().unwrap(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); } #[test] @@ -127,7 +131,7 @@ fn test_minimum_cardinality_key_invalid_config() { #[test] fn test_minimum_cardinality_key_empty_deps() { // No FDs: closure(K) = K. Only K = {0,1,2} determines all attributes. - let problem = MinimumCardinalityKey::new(3, vec![]); + let problem = MinimumCardinalityKey::new(3, vec![]).unwrap(); assert_eq!( problem.evaluate(&vec![true, true, true]).unwrap(), Min(Some(3)) @@ -149,7 +153,7 @@ fn test_minimum_cardinality_key_empty_deps() { #[test] fn test_minimum_cardinality_key_empty_key_candidate() { - let problem = MinimumCardinalityKey::new(1, vec![(vec![], vec![0])]); + let problem = MinimumCardinalityKey::new(1, vec![(vec![], vec![0])]).unwrap(); // Empty set is a key (closure of {} includes 0 via the FD {} -> {0}). assert_eq!(problem.evaluate(&vec![false]).unwrap(), Min(Some(0))); // Selecting attr 0 is also a key, but with cardinality 1. @@ -162,9 +166,8 @@ fn test_minimum_cardinality_key_empty_key_candidate() { } #[test] -#[should_panic(expected = "outside attribute set")] -fn test_minimum_cardinality_key_panics_on_invalid_index() { - MinimumCardinalityKey::new(3, vec![(vec![0, 3], vec![1])]); +fn test_minimum_cardinality_key_rejects_invalid_index() { + assert!(MinimumCardinalityKey::new(3, vec![(vec![0, 3], vec![1])]).is_err()); } #[test] @@ -177,3 +180,10 @@ fn test_minimum_cardinality_key_paper_example() { let witness = solver.solve(&problem).unwrap().unwrap(); assert_eq!(witness, solution); } + +#[test] +fn json_rejects_invalid_instance() { + let json = serde_json::json!({"num_attributes":3,"dependencies":[[[0,3],[1]]]}); + assert!(serde_json::from_value::(json.clone()).is_err()); + assert!(crate::registry::load_dyn("MinimumCardinalityKey", &Default::default(), json).is_err()); +} diff --git a/src/unit_tests/models/set/minimum_hitting_set.rs b/src/unit_tests/models/set/minimum_hitting_set.rs index 2b7a59b5f..f3f4fa4c7 100644 --- a/src/unit_tests/models/set/minimum_hitting_set.rs +++ b/src/unit_tests/models/set/minimum_hitting_set.rs @@ -18,6 +18,7 @@ fn issue_example_problem() -> MinimumHittingSet { vec![1, 4], ], ) + .unwrap() } #[test] @@ -37,12 +38,15 @@ fn issue_example_config() -> Vec { #[test] fn test_minimum_hitting_set_creation_accessors_and_dimensions() { - let problem = MinimumHittingSet::new(4, vec![vec![2, 1, 1], vec![3]]); + let problem = MinimumHittingSet::new(4, vec![vec![2, 1, 1], vec![3]]).unwrap(); assert_eq!(problem.universe_size(), 4); assert_eq!(problem.num_sets(), 2); - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!(problem.sets(), &[vec![1, 2], vec![3]]); assert_eq!(problem.get_set(0), Some(&vec![1, 2])); assert_eq!(problem.get_set(1), Some(&vec![3])); @@ -51,7 +55,7 @@ fn test_minimum_hitting_set_creation_accessors_and_dimensions() { #[test] fn test_minimum_hitting_set_evaluate_valid_and_invalid() { - let problem = MinimumHittingSet::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]); + let problem = MinimumHittingSet::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]).unwrap(); assert_eq!( problem.selected_elements(&[false, true, false, true]), @@ -76,7 +80,7 @@ fn test_minimum_hitting_set_evaluate_valid_and_invalid() { #[test] fn test_minimum_hitting_set_empty_set_is_always_invalid() { - let problem = MinimumHittingSet::new(3, vec![vec![0, 1], vec![]]); + let problem = MinimumHittingSet::new(3, vec![vec![0, 1], vec![]]).unwrap(); assert_eq!( problem.evaluate(&vec![true, true, true]).unwrap(), @@ -90,15 +94,14 @@ fn test_minimum_hitting_set_empty_set_is_always_invalid() { #[test] fn test_minimum_hitting_set_constructor_normalizes_sets() { - let problem = MinimumHittingSet::new(5, vec![vec![3, 1, 3, 2], vec![4, 0, 0], vec![]]); + let problem = MinimumHittingSet::new(5, vec![vec![3, 1, 3, 2], vec![4, 0, 0], vec![]]).unwrap(); assert_eq!(problem.sets(), &[vec![1, 2, 3], vec![0, 4], vec![]]); } #[test] -#[should_panic(expected = "outside universe")] fn test_minimum_hitting_set_rejects_out_of_range_elements() { - MinimumHittingSet::new(3, vec![vec![0, 3]]); + assert!(MinimumHittingSet::new(3, vec![vec![0, 3]]).is_err()); } #[test] @@ -119,7 +122,7 @@ fn test_minimum_hitting_set_bruteforce_optimum_issue_example() { #[test] fn test_minimum_hitting_set_serialization_round_trip() { - let problem = MinimumHittingSet::new(4, vec![vec![2, 1, 1], vec![3, 0]]); + let problem = MinimumHittingSet::new(4, vec![vec![2, 1, 1], vec![3, 0]]).unwrap(); let json = serde_json::to_string(&problem).unwrap(); let deserialized: MinimumHittingSet = serde_json::from_str(&json).unwrap(); @@ -176,3 +179,10 @@ fn test_minimum_hitting_set_canonical_example_spec() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), Min(Some(3))); } + +#[test] +fn json_rejects_invalid_instance() { + let json = serde_json::json!({"universe_size":3,"sets":[[0,3]]}); + assert!(serde_json::from_value::(json.clone()).is_err()); + assert!(crate::registry::load_dyn("MinimumHittingSet", &Default::default(), json).is_err()); +} diff --git a/src/unit_tests/models/set/minimum_set_covering.rs b/src/unit_tests/models/set/minimum_set_covering.rs index 299647f7c..91c74c615 100644 --- a/src/unit_tests/models/set/minimum_set_covering.rs +++ b/src/unit_tests/models/set/minimum_set_covering.rs @@ -1,9 +1,9 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; -include!("../../jl_helpers.rs"); #[test] fn test_minimum_set_covering_create_spec_uses_subsets_input() { @@ -20,21 +20,24 @@ fn test_minimum_set_covering_create_spec_uses_subsets_input() { #[test] fn test_set_covering_creation() { - let problem = MinimumSetCovering::::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]); + let problem = + MinimumSetCovering::::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]).unwrap(); assert_eq!(problem.universe_size(), 4); assert_eq!(problem.num_sets(), 3); - assert_eq!(problem.num_variables(), 3); + assert_eq!(problem.num_variables().unwrap(), 3); } #[test] fn test_set_covering_with_weights() { - let problem = MinimumSetCovering::with_weights(3, vec![vec![0, 1], vec![1, 2]], vec![5, 10]); + let problem = + MinimumSetCovering::with_weights(3, vec![vec![0, 1], vec![1, 2]], vec![5, 10]).unwrap(); assert_eq!(problem.weights_ref(), &vec![5, 10]); } #[test] fn test_covered_elements() { - let problem = MinimumSetCovering::::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]); + let problem = + MinimumSetCovering::::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]).unwrap(); let covered = problem.covered_elements(&[true, false, false]); assert!(covered.contains(&0)); @@ -60,7 +63,7 @@ fn test_is_set_cover_function() { #[test] fn test_get_set() { - let problem = MinimumSetCovering::::new(4, vec![vec![0, 1], vec![2, 3]]); + let problem = MinimumSetCovering::::new(4, vec![vec![0, 1], vec![2, 3]]).unwrap(); assert_eq!(problem.get_set(0), Some(&vec![0, 1])); assert_eq!(problem.get_set(1), Some(&vec![2, 3])); assert_eq!(problem.get_set(2), None); @@ -68,7 +71,7 @@ fn test_get_set() { #[test] fn test_empty_universe() { - let problem = MinimumSetCovering::::new(0, vec![]); + let problem = MinimumSetCovering::::new(0, vec![]).unwrap(); // Empty universe is trivially covered with size 0 assert_eq!(Problem::evaluate(&problem, &vec![]).unwrap(), Min(Some(0))); } @@ -87,7 +90,8 @@ fn test_jl_parity_evaluation() { let universe_size = instance["instance"]["universe_size"].as_u64().unwrap() as usize; let sets = jl_parse_sets(&instance["instance"]["sets"]); let weights = jl_parse_i64_vec(&instance["instance"]["weights"]); - let problem = MinimumSetCovering::::with_weights(universe_size, sets, weights); + let problem = + MinimumSetCovering::::with_weights(universe_size, sets, weights).unwrap(); for eval in instance["evaluations"].as_array().unwrap() { let config = jl_parse_bool_config(&eval["config"]); let result = problem.evaluate(&config).unwrap(); @@ -118,7 +122,8 @@ fn test_jl_parity_evaluation() { #[test] fn test_is_valid_solution() { // Universe: {0,1,2,3}, Sets: {0,1}, {1,2}, {2,3} - let problem = MinimumSetCovering::::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]); + let problem = + MinimumSetCovering::::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]).unwrap(); // Valid: all sets selected covers {0,1,2,3} assert!(problem.is_valid_solution(&[true, true, true])); // Invalid: only set 1 ({1,2}) doesn't cover 0 and 3 @@ -128,7 +133,8 @@ fn test_is_valid_solution() { #[test] fn test_setcovering_paper_example() { // Paper: U=5, sets {0,1,2},{1,3},{2,3,4}, min cover {S_0,S_2}, weight=2 - let problem = MinimumSetCovering::::new(5, vec![vec![0, 1, 2], vec![1, 3], vec![2, 3, 4]]); + let problem = + MinimumSetCovering::::new(5, vec![vec![0, 1, 2], vec![1, 3], vec![2, 3, 4]]).unwrap(); let config = vec![true, false, true]; // {S_0, S_2} covers all of {0,1,2,3,4} let result = problem.evaluate(&config).unwrap(); assert!(result.is_valid()); @@ -138,3 +144,15 @@ fn test_setcovering_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap().unwrap(), 2); } + +#[test] +fn construction_and_json_reject_invalid_data() { + assert!(MinimumSetCovering::::new(2, vec![vec![2]]).is_err()); + assert!(MinimumSetCovering::with_weights(2, vec![vec![0]], Vec::::new()).is_err()); + for json in [ + serde_json::json!({"universe_size":2,"sets":[[2]],"weights":[1]}), + serde_json::json!({"universe_size":2,"sets":[[0]],"weights":[]}), + ] { + assert!(serde_json::from_value::>(json).is_err()); + } +} diff --git a/src/unit_tests/models/set/prime_attribute_name.rs b/src/unit_tests/models/set/prime_attribute_name.rs index e9550959a..b9fcb389b 100644 --- a/src/unit_tests/models/set/prime_attribute_name.rs +++ b/src/unit_tests/models/set/prime_attribute_name.rs @@ -30,12 +30,13 @@ fn example1() -> PrimeAttributeName { ], 3, ) + .unwrap() } /// Helper: Issue Example 2 — 6 attributes, 1 FD, query=3 /// Only candidate key: {0,1} — attribute 3 is NOT prime fn example2() -> PrimeAttributeName { - PrimeAttributeName::new(6, vec![(vec![0, 1], vec![2, 3, 4, 5])], 3) + PrimeAttributeName::new(6, vec![(vec![0, 1], vec![2, 3, 4, 5])], 3).unwrap() } #[test] @@ -44,8 +45,11 @@ fn test_prime_attribute_name_creation() { assert_eq!(problem.num_attributes(), 6); assert_eq!(problem.num_dependencies(), 3); assert_eq!(problem.query_attribute(), 3); - assert_eq!(problem.num_variables(), 6); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2, 2, 2]); + assert_eq!(problem.num_variables().unwrap(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2, 2, 2] + ); assert_eq!(problem.dependencies().len(), 3); } @@ -186,7 +190,8 @@ fn test_prime_attribute_name_compute_closure_transitive() { 4, vec![(vec![0], vec![1]), (vec![1], vec![2]), (vec![2], vec![3])], 0, - ); + ) + .unwrap(); let mut attrs = vec![false; 4]; attrs[0] = true; let closure = problem.compute_closure(&attrs); @@ -194,19 +199,24 @@ fn test_prime_attribute_name_compute_closure_transitive() { } #[test] -#[should_panic(expected = "Query attribute")] fn test_prime_attribute_name_invalid_query() { - PrimeAttributeName::new(3, vec![(vec![0], vec![1, 2])], 5); + assert!(PrimeAttributeName::new(3, vec![(vec![0], vec![1, 2])], 5).is_err()); } #[test] -#[should_panic(expected = "empty LHS")] fn test_prime_attribute_name_empty_lhs() { - PrimeAttributeName::new(3, vec![(vec![], vec![1, 2])], 0); + assert!(PrimeAttributeName::new(3, vec![(vec![], vec![1, 2])], 0).is_err()); } #[test] -#[should_panic(expected = "outside attribute set")] fn test_prime_attribute_name_dep_out_of_range() { - PrimeAttributeName::new(3, vec![(vec![0], vec![5])], 0); + assert!(PrimeAttributeName::new(3, vec![(vec![0], vec![5])], 0).is_err()); +} + +#[test] +fn json_rejects_invalid_instance() { + let json = + serde_json::json!({"num_attributes":3,"dependencies":[[[],[1]]],"query_attribute":0}); + assert!(serde_json::from_value::(json.clone()).is_err()); + assert!(crate::registry::load_dyn("PrimeAttributeName", &Default::default(), json).is_err()); } 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 944ef4500..e87bb7c4a 100644 --- a/src/unit_tests/models/set/rooted_tree_storage_assignment.rs +++ b/src/unit_tests/models/set/rooted_tree_storage_assignment.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; fn yes_instance(bound: i64) -> RootedTreeStorageAssignment { @@ -21,7 +20,10 @@ fn test_rooted_tree_storage_assignment_creation() { problem.subsets(), &[vec![0, 2], vec![1, 3], vec![0, 4], vec![2, 4]] ); - assert_eq!(problem.dimensions(), vec![5; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 5] + ); } #[test] diff --git a/src/unit_tests/models/set/set_basis.rs b/src/unit_tests/models/set/set_basis.rs index a1c0aabdd..2714d6619 100644 --- a/src/unit_tests/models/set/set_basis.rs +++ b/src/unit_tests/models/set/set_basis.rs @@ -10,6 +10,7 @@ fn issue_example_problem(k: usize) -> SetBasis { vec![vec![0, 1], vec![1, 2], vec![0, 2], vec![0, 1, 2]], k, ) + .unwrap() } #[test] @@ -38,8 +39,11 @@ fn test_set_basis_creation() { assert_eq!(problem.universe_size(), 4); assert_eq!(problem.num_sets(), 4); assert_eq!(problem.basis_size(), 3); - assert_eq!(problem.num_variables(), 12); - assert_eq!(problem.dimensions(), vec![2; 12]); + assert_eq!(problem.num_variables().unwrap(), 12); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 12] + ); assert_eq!(problem.get_set(0), Some(&vec![0, 1])); assert_eq!(problem.get_set(4), None); } @@ -130,20 +134,6 @@ fn test_set_basis_rejects_wrong_config_length() { assert!(problem.evaluate(&solution).is_err()); } -#[test] -fn test_set_basis_deserialized_invalid_target_returns_false() { - let problem: SetBasis = serde_json::from_value(serde_json::json!({ - "universe_size": 4, - "collection": [[0, 4]], - "k": 1 - })) - .unwrap(); - - assert!(!problem - .evaluate(&vec![vec![true, false, false, false]]) - .unwrap()); -} - #[test] fn test_set_basis_deserialized_unsorted_target_still_evaluates_correctly() { let problem: SetBasis = serde_json::from_value(serde_json::json!({ @@ -153,13 +143,13 @@ fn test_set_basis_deserialized_unsorted_target_still_evaluates_correctly() { })) .unwrap(); + assert_eq!(problem.collection(), &[vec![0, 1]]); assert!(problem.evaluate(&vec![vec![true, true]]).unwrap()); } #[test] -#[should_panic(expected = "outside universe")] fn test_set_basis_rejects_out_of_range_elements() { - SetBasis::new(4, vec![vec![0, 4]], 1); + assert!(SetBasis::new(4, vec![vec![0, 4]], 1).is_err()); } #[test] @@ -167,7 +157,7 @@ fn test_set_basis_basis_not_subset_of_target() { // Basis = {{0, 2}}, target = {{0, 1}}. // The basis set {0, 2} is NOT a subset of {0, 1} (element 2 not in target), // so it should not be used, and the target cannot be covered. - let problem = SetBasis::new(3, vec![vec![0, 1]], 1); + let problem = SetBasis::new(3, vec![vec![0, 1]], 1).unwrap(); // Config encodes basis set {0, 2}: bits [1, 0, 1] assert!(!problem.evaluate(&vec![vec![true, false, true]]).unwrap()); } @@ -182,26 +172,39 @@ fn test_set_basis_is_valid_solution() { #[test] fn test_set_basis_k_zero_empty_collection() { // k = 0 with empty collection: trivially satisfiable (no targets to cover). - let problem = SetBasis::new(3, vec![], 0); - assert_eq!(problem.dimensions(), Vec::::new()); + let problem = SetBasis::new(3, vec![], 0).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } #[test] fn test_set_basis_k_zero_nonempty_collection() { // k = 0 with non-empty collection: impossible (no basis sets to cover targets). - let problem = SetBasis::new(3, vec![vec![0, 1]], 0); - assert_eq!(problem.dimensions(), Vec::::new()); + let problem = SetBasis::new(3, vec![vec![0, 1]], 0).unwrap(); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(!problem.evaluate(&vec![]).unwrap()); } #[test] fn test_set_basis_empty_collection_with_k_positive() { // Empty collection with k > 0: trivially satisfiable (no targets to cover). - let problem = SetBasis::new(2, vec![], 2); + let problem = SetBasis::new(2, vec![], 2).unwrap(); assert_eq!(problem.basis_size(), 2); assert_eq!(problem.num_sets(), 0); // Any valid config of length k * universe_size = 4 should satisfy. assert!(problem.evaluate(&vec![vec![false; 2]; 2]).unwrap()); assert!(problem.evaluate(&vec![vec![true; 2]; 2]).unwrap()); } + +#[test] +fn json_rejects_invalid_instance() { + let json = serde_json::json!({"universe_size":3,"collection":[[0,3]],"k":1}); + assert!(serde_json::from_value::(json.clone()).is_err()); + assert!(crate::registry::load_dyn("SetBasis", &Default::default(), json).is_err()); +} diff --git a/src/unit_tests/models/set/set_splitting.rs b/src/unit_tests/models/set/set_splitting.rs index 476e97af4..ca465327b 100644 --- a/src/unit_tests/models/set/set_splitting.rs +++ b/src/unit_tests/models/set/set_splitting.rs @@ -9,7 +9,7 @@ fn test_set_splitting_creation() { let problem = SetSplitting::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]); assert_eq!(problem.universe_size(), 4); assert_eq!(problem.num_subsets(), 3); - assert_eq!(problem.num_variables(), 4); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] diff --git a/src/unit_tests/models/set/three_dimensional_matching.rs b/src/unit_tests/models/set/three_dimensional_matching.rs index 3ef8b0df4..d8280f5ee 100644 --- a/src/unit_tests/models/set/three_dimensional_matching.rs +++ b/src/unit_tests/models/set/three_dimensional_matching.rs @@ -8,11 +8,15 @@ fn test_three_dimensional_matching_creation() { let problem = ThreeDimensionalMatching::new( 3, vec![(0, 1, 2), (1, 0, 1), (2, 2, 0), (0, 0, 0), (1, 2, 2)], - ); + ) + .unwrap(); assert_eq!(problem.universe_size(), 3); assert_eq!(problem.num_triples(), 5); - assert_eq!(problem.num_variables(), 5); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2, 2]); + assert_eq!(problem.num_variables().unwrap(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2, 2] + ); } #[test] @@ -22,7 +26,8 @@ fn test_three_dimensional_matching_evaluation() { let problem = ThreeDimensionalMatching::new( 3, vec![(0, 1, 2), (1, 0, 1), (2, 2, 0), (0, 0, 0), (1, 2, 2)], - ); + ) + .unwrap(); // T0, T1, T2: W={0,1,2} distinct, X={1,0,2} distinct, Y={2,1,0} distinct -> valid assert!(problem @@ -60,7 +65,8 @@ fn test_three_dimensional_matching_solver() { let problem = ThreeDimensionalMatching::new( 3, vec![(0, 1, 2), (1, 0, 1), (2, 2, 0), (0, 0, 0), (1, 2, 2)], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -76,7 +82,7 @@ fn test_three_dimensional_matching_solver() { #[test] fn test_three_dimensional_matching_no_solution() { // q = 2, all triples share w=0 -> no matching of size 2 possible - let problem = ThreeDimensionalMatching::new(2, vec![(0, 0, 0), (0, 1, 1), (0, 0, 1)]); + let problem = ThreeDimensionalMatching::new(2, vec![(0, 0, 0), (0, 1, 1), (0, 0, 1)]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -85,7 +91,7 @@ fn test_three_dimensional_matching_no_solution() { #[test] fn test_three_dimensional_matching_serialization() { - let problem = ThreeDimensionalMatching::new(2, vec![(0, 1, 0), (1, 0, 1)]); + let problem = ThreeDimensionalMatching::new(2, vec![(0, 1, 0), (1, 0, 1)]).unwrap(); let json = serde_json::to_string(&problem).unwrap(); let deserialized: ThreeDimensionalMatching = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.universe_size(), problem.universe_size()); @@ -96,7 +102,7 @@ fn test_three_dimensional_matching_serialization() { #[test] fn test_three_dimensional_matching_empty() { // q = 0: trivially satisfiable - let problem = ThreeDimensionalMatching::new(0, vec![]); + let problem = ThreeDimensionalMatching::new(0, vec![]).unwrap(); assert!(problem.evaluate(&vec![]).unwrap()); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -105,7 +111,7 @@ fn test_three_dimensional_matching_empty() { #[test] fn test_three_dimensional_matching_get_triple() { - let problem = ThreeDimensionalMatching::new(2, vec![(0, 1, 0), (1, 0, 1)]); + let problem = ThreeDimensionalMatching::new(2, vec![(0, 1, 0), (1, 0, 1)]).unwrap(); assert_eq!(problem.get_triple(0), Some(&(0, 1, 0))); assert_eq!(problem.get_triple(1), Some(&(1, 0, 1))); assert_eq!(problem.get_triple(2), None); @@ -113,7 +119,7 @@ fn test_three_dimensional_matching_get_triple() { #[test] fn test_three_dimensional_matching_rejects_wrong_config_length() { - let problem = ThreeDimensionalMatching::new(2, vec![(0, 1, 0), (1, 0, 1)]); + let problem = ThreeDimensionalMatching::new(2, vec![(0, 1, 0), (1, 0, 1)]).unwrap(); assert!(matches!( problem.evaluate(&vec![true, true, false]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -122,21 +128,20 @@ fn test_three_dimensional_matching_rejects_wrong_config_length() { #[test] fn test_three_dimensional_matching_rejects_non_binary_config_values() { - let problem = ThreeDimensionalMatching::new(2, vec![(0, 1, 0), (1, 0, 1)]); + let problem = ThreeDimensionalMatching::new(2, vec![(0, 1, 0), (1, 0, 1)]).unwrap(); assert!( crate::registry::DynProblem::evaluate_dyn(&problem, &serde_json::json!([true, 2])).is_err() ); } #[test] -#[should_panic(expected = "outside 0..")] fn test_three_dimensional_matching_element_out_of_range() { - ThreeDimensionalMatching::new(2, vec![(0, 3, 0)]); + assert!(ThreeDimensionalMatching::new(2, vec![(0, 3, 0)]).is_err()); } #[test] fn test_three_dimensional_matching_is_valid_solution() { - let problem = ThreeDimensionalMatching::new(2, vec![(0, 1, 0), (1, 0, 1)]); + let problem = ThreeDimensionalMatching::new(2, vec![(0, 1, 0), (1, 0, 1)]).unwrap(); assert!(problem.evaluate(&vec![true, true]).unwrap().0); assert!(!problem.evaluate(&vec![true, false]).unwrap().0); } @@ -146,9 +151,18 @@ fn test_three_dimensional_matching_duplicate_coordinates() { // q = 2, T0=(0,0,0), T1=(1,1,1), T2=(0,1,0) // T0+T1 is valid matching; T0+T2 shares w=0; T1+T2 shares y (not y, T1 y=1, T2 y=0, ok) // Actually T1+T2: w={1,0} ok, x={1,1} NOT distinct -> invalid - let problem = ThreeDimensionalMatching::new(2, vec![(0, 0, 0), (1, 1, 1), (0, 1, 0)]); + let problem = ThreeDimensionalMatching::new(2, vec![(0, 0, 0), (1, 1, 1), (0, 1, 0)]).unwrap(); assert!(problem.evaluate(&vec![true, true, false]).unwrap()); // T0+T1: w={0,1}, x={0,1}, y={0,1} all distinct assert!(!problem.evaluate(&vec![true, false, true]).unwrap()); // T0+T2: w={0,0} not distinct assert!(!problem.evaluate(&vec![false, true, true]).unwrap()); // T1+T2: x={1,1} not distinct } + +#[test] +fn json_rejects_invalid_instance() { + let json = serde_json::json!({"universe_size":2,"triples":[[0,2,0]]}); + assert!(serde_json::from_value::(json.clone()).is_err()); + assert!( + crate::registry::load_dyn("ThreeDimensionalMatching", &Default::default(), json).is_err() + ); +} 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 b67594e2a..57c45aee6 100644 --- a/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs +++ b/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs @@ -17,8 +17,11 @@ fn test_two_dimensional_consecutive_sets_creation() { ); assert_eq!(problem.alphabet_size(), 6); assert_eq!(problem.num_subsets(), 5); - assert_eq!(problem.num_variables(), 6); - assert_eq!(problem.dimensions(), vec![6, 6, 6, 6, 6, 6]); + assert_eq!(problem.num_variables().unwrap(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6, 6, 6, 6, 6, 6] + ); } #[test] diff --git a/src/unit_tests/prelude.rs b/src/unit_tests/prelude.rs index 6e7295b53..0b453fbe4 100644 --- a/src/unit_tests/prelude.rs +++ b/src/unit_tests/prelude.rs @@ -3,18 +3,20 @@ use crate::topology::SimpleGraph; #[test] fn test_prelude_exports_rectilinear_picture_compression() { - let problem = RectilinearPictureCompression::new(vec![vec![true]], 1); + let problem = RectilinearPictureCompression::new(vec![vec![true]], 1).unwrap(); assert_eq!(problem.bound(), 1); } #[test] fn test_prelude_exports_partition_into_cliques() { - let problem = PartitionIntoCliques::new(SimpleGraph::new(2, vec![(0, 1)]), 1); + let problem = PartitionIntoCliques::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 1).unwrap(); assert_eq!(problem.num_cliques(), 1); } #[test] fn test_prelude_exports_degree_constrained_spanning_tree() { - let problem = DegreeConstrainedSpanningTree::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 2); + let problem = + DegreeConstrainedSpanningTree::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 2) + .unwrap(); assert_eq!(problem.max_degree(), 2); } diff --git a/src/unit_tests/problem_parameters.rs b/src/unit_tests/problem_parameters.rs index 200f0a081..eff2eab1d 100644 --- a/src/unit_tests/problem_parameters.rs +++ b/src/unit_tests/problem_parameters.rs @@ -10,8 +10,8 @@ use crate::traits::Problem; #[test] fn test_problem_parameters_mis() { - let g = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); - let mis = MaximumIndependentSet::new(g, vec![1i64; 4]); + let g = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); + let mis = MaximumIndependentSet::new(g, vec![1i64; 4]).unwrap(); let size = mis.parameters(); assert_eq!(size.get("num_vertices"), Some(4)); assert_eq!(size.get("num_edges"), Some(3)); @@ -19,8 +19,8 @@ fn test_problem_parameters_mis() { #[test] fn test_problem_parameters_max_clique() { - let g = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let mc = MaximumClique::new(g, vec![1i64; 3]); + let g = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let mc = MaximumClique::new(g, vec![1i64; 3]).unwrap(); let size = mc.parameters(); assert_eq!(size.get("num_vertices"), Some(3)); assert_eq!(size.get("num_edges"), Some(3)); @@ -28,8 +28,8 @@ fn test_problem_parameters_max_clique() { #[test] fn test_problem_parameters_min_vc() { - let g = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let mvc = MinimumVertexCover::new(g, vec![1i64; 3]); + let g = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let mvc = MinimumVertexCover::new(g, vec![1i64; 3]).unwrap(); let size = mvc.parameters(); assert_eq!(size.get("num_vertices"), Some(3)); assert_eq!(size.get("num_edges"), Some(2)); @@ -37,8 +37,8 @@ fn test_problem_parameters_min_vc() { #[test] fn test_problem_parameters_min_ds() { - let g = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); - let mds = MinimumDominatingSet::new(g, vec![1i64; 4]); + let g = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(); + let mds = MinimumDominatingSet::new(g, vec![1i64; 4]).unwrap(); let size = mds.parameters(); assert_eq!(size.get("num_vertices"), Some(4)); assert_eq!(size.get("num_edges"), Some(3)); @@ -46,8 +46,8 @@ fn test_problem_parameters_min_ds() { #[test] fn test_problem_parameters_max_cut() { - let g = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let mc = MaxCut::new(g, vec![1i64; 3]); + let g = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let mc = MaxCut::new(g, vec![1i64; 3]).unwrap(); let size = mc.parameters(); assert_eq!(size.get("num_vertices"), Some(3)); assert_eq!(size.get("num_edges"), Some(3)); @@ -55,8 +55,8 @@ fn test_problem_parameters_max_cut() { #[test] fn test_problem_parameters_maximum_matching() { - let g = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); - let mm = MaximumMatching::new(g, vec![1i64; 3]); + let g = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); + let mm = MaximumMatching::new(g, vec![1i64; 3]).unwrap(); let size = mm.parameters(); assert_eq!(size.get("num_vertices"), Some(4)); assert_eq!(size.get("num_edges"), Some(3)); @@ -64,8 +64,8 @@ fn test_problem_parameters_maximum_matching() { #[test] fn test_problem_parameters_maximal_is() { - let g = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let mis = MaximalIS::new(g, vec![1i64; 3]); + let g = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let mis = MaximalIS::new(g, vec![1i64; 3]).unwrap(); let size = mis.parameters(); assert_eq!(size.get("num_vertices"), Some(3)); assert_eq!(size.get("num_edges"), Some(2)); @@ -73,7 +73,7 @@ fn test_problem_parameters_maximal_is() { #[test] fn test_problem_parameters_knapsack_capacity() { - let knapsack = Knapsack::new(vec![2, 3], vec![5, 7], 4); + let knapsack = Knapsack::new(vec![2, 3], vec![5, 7], 4).unwrap(); let parameters = knapsack.parameters(); assert_eq!(parameters.get("capacity"), Some(4)); @@ -83,7 +83,7 @@ fn test_problem_parameters_knapsack_capacity() { #[test] fn test_problem_parameters_kcoloring() { use crate::variant::KN; - let g = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let g = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); let kc = KColoring::::with_k(g, 3); let size = kc.parameters(); assert_eq!(size.get("num_vertices"), Some(3)); @@ -93,8 +93,8 @@ fn test_problem_parameters_kcoloring() { #[test] fn test_problem_parameters_tsp() { - let g = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let tsp = TravelingSalesman::new(g, vec![1i64; 3]); + let g = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let tsp = TravelingSalesman::new(g, vec![1i64; 3]).unwrap(); let size = tsp.parameters(); assert_eq!(size.get("num_vertices"), Some(3)); assert_eq!(size.get("num_edges"), Some(3)); @@ -188,7 +188,7 @@ fn test_problem_parameters_circuitsat() { #[test] fn test_problem_parameters_paintshop() { - let ps = PaintShop::new(vec!["a", "b", "a", "c", "c", "b"]); + let ps = PaintShop::new(vec!["a", "b", "a", "c", "c", "b"]).unwrap(); let size = ps.parameters(); assert_eq!(size.get("num_cars"), Some(3)); assert_eq!(size.get("num_sequence"), Some(6)); @@ -196,7 +196,10 @@ fn test_problem_parameters_paintshop() { #[test] fn test_problem_parameters_biclique_cover() { - let bc = BicliqueCover::new(BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (1, 2)]), 2); + let bc = BicliqueCover::new( + BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (1, 2)]).unwrap(), + 2, + ); let size = bc.parameters(); assert_eq!(size.get("left_size"), Some(2)); assert_eq!(size.get("right_size"), Some(3)); @@ -206,7 +209,7 @@ fn test_problem_parameters_biclique_cover() { #[test] fn test_problem_parameters_bmf() { - let bmf = BMF::new(vec![vec![true, false], vec![false, true]], 2); + let bmf = BMF::new(vec![vec![true, false], vec![false, true]], 2).unwrap(); let size = bmf.parameters(); assert_eq!(size.get("rows"), Some(2)); assert_eq!(size.get("cols"), Some(2)); @@ -223,7 +226,7 @@ fn test_problem_parameters_set_packing() { #[test] fn test_problem_parameters_set_covering() { - let sc = MinimumSetCovering::::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]); + let sc = MinimumSetCovering::::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]).unwrap(); let size = sc.parameters(); assert_eq!(size.get("num_sets"), Some(3)); assert_eq!(size.get("universe_size"), Some(4)); diff --git a/src/unit_tests/property.rs b/src/unit_tests/property.rs index 981f5e1de..d681bd1db 100644 --- a/src/unit_tests/property.rs +++ b/src/unit_tests/property.rs @@ -40,8 +40,8 @@ proptest! { /// is a minimum vertex cover, and their sizes sum to n. #[test] fn independent_set_complement_is_vertex_cover((n, edges) in graph_strategy(8)) { - let is_problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges.clone()), vec![1i64; n]); - let vc_problem = MinimumVertexCover::new(SimpleGraph::new(n, edges), vec![1i64; n]); + let is_problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges.clone()).unwrap(), vec![1i64; n]).unwrap(); + let vc_problem = MinimumVertexCover::new(SimpleGraph::new(n, edges).unwrap(), vec![1i64; n]).unwrap(); let solver = BruteForce::new(); let is_solutions = solver.find_all_witnesses(&is_problem).unwrap(); @@ -57,7 +57,7 @@ proptest! { /// Property: Any subset of a valid independent set is also a valid independent set. #[test] fn valid_solution_stays_valid_under_subset((n, edges) in graph_strategy(6)) { - let problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges), vec![1i64; n]); + let problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges).unwrap(), vec![1i64; n]).unwrap(); let solver = BruteForce::new(); for sol in solver.find_all_witnesses(&problem).unwrap() { @@ -74,7 +74,7 @@ proptest! { /// Property: A vertex cover with additional vertices is still a valid cover. #[test] fn vertex_cover_superset_is_valid((n, edges) in graph_strategy(6)) { - let problem = MinimumVertexCover::new(SimpleGraph::new(n, edges), vec![1i64; n]); + let problem = MinimumVertexCover::new(SimpleGraph::new(n, edges).unwrap(), vec![1i64; n]).unwrap(); let solver = BruteForce::new(); for sol in solver.find_all_witnesses(&problem).unwrap() { @@ -91,8 +91,8 @@ proptest! { /// Property: The complement of any valid independent set is a valid vertex cover. #[test] fn is_complement_is_vc((n, edges) in graph_strategy(7)) { - let is_problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges.clone()), vec![1i64; n]); - let vc_problem = MinimumVertexCover::new(SimpleGraph::new(n, edges), vec![1i64; n]); + let is_problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges.clone()).unwrap(), vec![1i64; n]).unwrap(); + let vc_problem = MinimumVertexCover::new(SimpleGraph::new(n, edges).unwrap(), vec![1i64; n]).unwrap(); let solver = BruteForce::new(); // Get all valid independent sets (not just optimal) @@ -107,7 +107,7 @@ proptest! { /// Property: Empty selection is always a valid (but possibly non-optimal) independent set. #[test] fn empty_is_always_valid_is((n, edges) in graph_strategy(10)) { - let problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges), vec![1i64; n]); + let problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges).unwrap(), vec![1i64; n]).unwrap(); let empty = vec![false; n]; // Valid configuration returns is_valid() == true (0 for empty set) prop_assert!(problem.evaluate(&empty).unwrap().is_valid()); @@ -117,7 +117,7 @@ proptest! { /// (when there is at least one vertex). #[test] fn full_is_always_valid_vc((n, edges) in graph_strategy(10)) { - let problem = MinimumVertexCover::new(SimpleGraph::new(n, edges), vec![1i64; n]); + let problem = MinimumVertexCover::new(SimpleGraph::new(n, edges).unwrap(), vec![1i64; n]).unwrap(); let full = vec![true; n]; // Valid configuration returns is_valid() == true prop_assert!(problem.evaluate(&full).unwrap().is_valid()); @@ -126,7 +126,7 @@ proptest! { /// Property: Solution size is non-negative for independent sets. #[test] fn is_size_non_negative((n, edges) in graph_strategy(8)) { - let problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges), vec![1i64; n]); + let problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges).unwrap(), vec![1i64; n]).unwrap(); let solver = BruteForce::new(); for sol in solver.find_all_witnesses(&problem).unwrap() { diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index 6595c0007..905712da3 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -362,7 +362,7 @@ fn test_direct_reduction_exists() { #[test] fn test_kcoloring_to_partitionintocliques_smoke() { - let source = KColoring::::with_k(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 2); + let source = KColoring::::with_k(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 2); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); assert_eq!(reduction.target_problem().num_cliques(), 2); @@ -926,17 +926,13 @@ fn test_decision_minimum_dominating_set_to_minmax_multicenter_has_direct_witness (2, vec![false, true, true, false, true, true], true), ] { let source = Decision::new( - MinimumDominatingSet::new(SimpleGraph::path(4), vec![One; 4]), + MinimumDominatingSet::new(SimpleGraph::path(4), vec![One; 4]).unwrap(), bound, ); - let aggregate = (edge.reduce_aggregate_fn.unwrap())(&source).unwrap(); + let step = (edge.reduce_fn.unwrap())(&source).unwrap(); assert_eq!( - *aggregate - .extract_value_from_solution_dyn(&witness) - .unwrap() - .downcast::() - .unwrap(), - Or(expected) + step.interpret_optimum.as_ref().unwrap()(&witness).unwrap(), + expected ); } } @@ -1064,16 +1060,18 @@ fn test_find_paths_bounded_returns_shortest_when_truncated() { fn edge() -> ReductionEdgeData { fn reduce( _source: &dyn std::any::Any, - ) -> std::result::Result< - Box, - crate::rules::ReductionError, - > { - Ok(Box::new(crate::rules::VariantReductionResult::< - crate::models::formula::Satisfiability, - crate::models::formula::Satisfiability, - >::new( - crate::models::formula::Satisfiability::new(0, vec![]), - ))) + ) -> std::result::Result + { + Ok(crate::rules::registry::ExecutedStep { + witness: std::rc::Rc::new(crate::rules::VariantReductionResult::< + crate::models::formula::Satisfiability, + crate::models::formula::Satisfiability, + >::new( + crate::models::formula::Satisfiability::new(0, vec![]) + )), + aggregate: None, + interpret_optimum: None, + }) } ReductionEdgeData { diff --git a/src/unit_tests/registry/dispatch.rs b/src/unit_tests/registry/dispatch.rs index 2291f949d..0c46b9509 100644 --- a/src/unit_tests/registry/dispatch.rs +++ b/src/unit_tests/registry/dispatch.rs @@ -49,8 +49,12 @@ impl Problem for SolutionProblem { } impl crate::solvers::BruteForceProblem for SolutionProblem { - fn dimensions(&self) -> Vec { - vec![2; self.weights.len()] + fn num_variables(&self) -> Result { + Ok(self.weights.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -77,7 +81,9 @@ inventory::submit! { #[test] fn test_dyn_problem_blanket_impl_exposes_problem_metadata() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); let dyn_problem: &dyn DynProblem = &problem; assert_eq!(dyn_problem.problem_name(), "MaximumIndependentSet"); @@ -93,51 +99,34 @@ fn test_dyn_problem_blanket_impl_exposes_problem_metadata() { } #[test] -fn test_dyn_problem_formats_optimization_values_as_max_min() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); +fn test_dyn_evaluation_distinguishes_infeasibility_and_malformed_input() { + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); let dyn_problem: &dyn DynProblem = &problem; - assert_eq!( dyn_problem .evaluate_dyn(&serde_json::json!([true, false, true])) .unwrap(), - "Max(2)" + ("Max(2)".into(), true) ); assert_eq!( dyn_problem .evaluate_dyn(&serde_json::json!([true, true, false])) .unwrap(), - "Max(None)" - ); -} - -#[test] -fn test_dyn_witness_evaluation_distinguishes_infeasibility_and_malformed_input() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); - let dyn_problem: &dyn DynProblem = &problem; - assert_eq!( - dyn_problem - .evaluate_witness_dyn(&serde_json::json!([true, false, true])) - .unwrap(), - Some("Max(2)".into()) - ); - assert_eq!( - dyn_problem - .evaluate_witness_dyn(&serde_json::json!([true, true, false])) - .unwrap(), - None + ("Max(None)".into(), false) ); assert!(dyn_problem - .evaluate_witness_dyn(&serde_json::json!([true])) + .evaluate_dyn(&serde_json::json!([true])) .is_err()); assert!(dyn_problem - .evaluate_witness_dyn(&serde_json::json!([0, 0, 0])) + .evaluate_dyn(&serde_json::json!([0, 0, 0])) .is_err()); } #[test] fn test_loaded_dyn_problem_delegates_to_solve_fn() { - let problem = SubsetSum::new(vec![3u32, 7u32, 1u32], 4u32); + let problem = SubsetSum::new(vec![3u32, 7u32, 1u32], 4u32).unwrap(); let loaded = LoadedDynProblem::new(Box::new(problem)); assert_eq!( @@ -177,7 +166,11 @@ fn loaded_dyn_problem_returns_solution_and_evaluation() { #[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![1i64; 3]); + let problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); let variant = BTreeMap::from([ ("graph".to_string(), "SimpleGraph".to_string()), ("weight".to_string(), "i64".to_string()), @@ -204,7 +197,9 @@ fn test_find_variant_entry_requires_exact_variant() { #[test] fn test_load_dyn_round_trips_maximum_independent_set() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); let variant = BTreeMap::from([ ("graph".to_string(), "SimpleGraph".to_string()), ("weight".to_string(), "i64".to_string()), @@ -229,7 +224,7 @@ fn test_load_dyn_round_trips_maximum_independent_set() { #[test] fn test_load_dyn_solves_subset_sum() { - let problem = SubsetSum::new(vec![3u32, 7u32, 1u32], 4u32); + let problem = SubsetSum::new(vec![3u32, 7u32, 1u32], 4u32).unwrap(); let variant = BTreeMap::new(); let loaded = load_dyn( "SubsetSum", @@ -247,7 +242,9 @@ fn test_load_dyn_solves_subset_sum() { #[test] fn test_load_dyn_rejects_partial_variant() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); let partial = BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]); let err = load_dyn( "MaximumIndependentSet", @@ -261,7 +258,9 @@ fn test_load_dyn_rejects_partial_variant() { #[test] fn test_load_dyn_rejects_alias_name() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); let variant = BTreeMap::from([ ("graph".to_string(), "SimpleGraph".to_string()), ("weight".to_string(), "i64".to_string()), @@ -271,7 +270,9 @@ fn test_load_dyn_rejects_alias_name() { #[test] fn test_serialize_any_round_trips_exact_variant() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); let variant = BTreeMap::from([ ("graph".to_string(), "SimpleGraph".to_string()), ("weight".to_string(), "i64".to_string()), @@ -282,7 +283,9 @@ fn test_serialize_any_round_trips_exact_variant() { #[test] fn test_serialize_any_rejects_partial_variant() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); let partial = BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]); assert!(serialize_any("MaximumIndependentSet", &partial, &problem as &dyn Any).is_none()); } @@ -300,7 +303,9 @@ fn test_format_metric_uses_display() { #[test] fn test_loaded_dyn_problem_debug() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); let variant = BTreeMap::from([ ("graph".to_string(), "SimpleGraph".to_string()), ("weight".to_string(), "i64".to_string()), @@ -370,7 +375,10 @@ fn explicit_independent_set_variants_round_trip_through_standard_api() { "Max(2.5)" }; assert_eq!(evaluation, expected, "{variant:?}"); - assert_eq!(loaded.evaluate_dyn(&solution).unwrap(), expected); + assert_eq!( + loaded.evaluate_dyn(&solution).unwrap(), + (expected.into(), true) + ); } } let mut bad = base.clone(); @@ -417,3 +425,63 @@ fn registered_weight_variants_reject_invalid_graphs_and_witnesses() { } } } + +#[derive(Clone, serde::Serialize)] +struct DirectEvaluation; + +#[derive(Clone, serde::Serialize)] +struct DirectValue(bool); + +impl DirectValue { + fn is_valid(&self) -> bool { + self.0 + } +} + +impl std::fmt::Display for DirectValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Problem for DirectEvaluation { + const NAME: &'static str = "DirectEvaluation"; + type Solution = bool; + type Value = DirectValue; + + fn parameter_names() -> &'static [&'static str] { + &[] + } + fn parameters(&self) -> crate::types::ProblemParameters { + Default::default() + } + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } + fn evaluate(&self, solution: &bool) -> Result { + Ok(DirectValue(*solution)) + } +} + +crate::impl_dyn_problem!(DirectEvaluation); + +#[test] +fn dynamic_evaluation_needs_neither_aggregation_nor_registration() { + let problem: &dyn DynProblem = &DirectEvaluation; + for feasible in [true, false] { + let input = serde_json::json!(feasible); + assert_eq!( + problem.evaluate_dyn(&input).unwrap(), + (feasible.to_string(), feasible) + ); + assert_eq!(problem.evaluate_json(&input).unwrap(), input); + } + assert!(problem.evaluate_dyn(&serde_json::json!([])).is_err()); + assert!(problem.evaluate_json(&serde_json::json!([])).is_err()); + assert_eq!(problem.problem_name(), "DirectEvaluation"); + assert!(problem.variant_map().is_empty()); + assert!(problem.parameter_names_dyn().is_empty()); + assert_eq!(problem.parameters_dyn(), Default::default()); + assert_eq!(problem.serialize_json(), serde_json::Value::Null); + assert!(problem.as_any().is::()); +} diff --git a/src/unit_tests/registry/variant.rs b/src/unit_tests/registry/variant.rs index eb909bf25..4dcbca44c 100644 --- a/src/unit_tests/registry/variant.rs +++ b/src/unit_tests/registry/variant.rs @@ -320,7 +320,7 @@ fn established_random_generation_models_remain_registered() { DecisionMinimumVertexCover MaximumIndependentSet MinimumVertexCover MaximumClique MinimumDominatingSet MaximalIS KClique MinimumCutIntoBoundedSets HamiltonianCircuit HamiltonianPath HamiltonianPathBetweenTwoVertices LongestCircuit MinimumMaximalMatching - RootedTreeArrangement SteinerTree SteinerTreeInGraphs LengthBoundedDisjointPaths + RootedTreeArrangement SteinerTree LengthBoundedDisjointPaths MaximumAchromaticNumber MaximumDomaticNumber MinimumCoveringByCliques MinimumIntersectionGraphBasis MaximumLeafSpanningTree GeneralizedHex BottleneckTravelingSalesman MaxCut MaximumMatching TravelingSalesman SpinGlass KColoring @@ -367,7 +367,7 @@ fn unit_variants_construct_without_unit_inputs() { "MaximumCoKPlex" => json!({"graph":graph,"k":1}), "MinimumFeedbackVertexSet" => json!({"graph":{"num_vertices":3,"arcs":[[0,1],[1,2]]}}), "MaximumSetPacking" => json!({"subsets":[[0,1],[1,2]]}), - "SteinerTree" | "SteinerTreeInGraphs" => json!({"graph":graph,"terminals":[0,2]}), + "SteinerTree" => json!({"graph":graph,"terminals":[0,2]}), "MaximumIndependentSet" => match entry.variant_map()["graph"].as_str() { "SimpleGraph" => json!({"graph":[[0,1],[1,2]]}), "KingsSubgraph" => json!({"positions":[[0,0],[1,0],[2,0]]}), @@ -377,7 +377,9 @@ fn unit_variants_construct_without_unit_inputs() { graph => panic!("missing construction case for {graph}"), }, "DecisionMaximumIndependentSet" => json!({"graph":[[0,1],[1,2]],"bound":2}), - "DecisionMinimumDominatingSet" => json!({"graph":graph,"bound":1}), + "DecisionMinimumDominatingSet" | "DecisionMinimumVertexCover" => { + json!({"graph":graph,"bound":1}) + } "MaxCut" => json!({"graph":[[0,1],[1,2]]}), "LongestPath" => json!({"graph":[[0,1],[1,2]],"source_vertex":0,"target_vertex":2}), "MinMaxMulticenter" => json!({"graph":[[0,1],[1,2]],"k":1}), @@ -435,13 +437,9 @@ fn unit_construction_preserves_model_validation() { let graph = json!({"num_vertices":3,"edges":[[0,1],[1,2]]}); for (name, data) in [ ("MaximumCoKPlex", json!({"graph":graph,"k":0})), - ("SteinerTree", json!({"graph":graph,"terminals":[0]})), + ("SteinerTree", json!({"graph":graph,"terminals":[]})), ("SteinerTree", json!({"graph":graph,"terminals":[0,0]})), ("SteinerTree", json!({"graph":graph,"terminals":[0,3]})), - ( - "SteinerTreeInGraphs", - json!({"graph":graph,"terminals":[3]}), - ), ( "MinimumTardinessSequencing", json!({"deadlines":[1,2],"precedences":[[0,2]]}), diff --git a/src/unit_tests/rules/acyclicpartition_ilp.rs b/src/unit_tests/rules/acyclicpartition_ilp.rs index 467d43aec..f95d8bf14 100644 --- a/src/unit_tests/rules/acyclicpartition_ilp.rs +++ b/src/unit_tests/rules/acyclicpartition_ilp.rs @@ -9,12 +9,13 @@ use crate::traits::Problem; fn small_instance() -> AcyclicPartition { // Chain 0->1->2->3, unit weights, unit arc costs, B=3, K=2 AcyclicPartition::new( - DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1, 1, 1, 1], vec![1, 1, 1], 3, 2, ) + .unwrap() } #[test] @@ -72,17 +73,21 @@ fn test_infeasible_instance() { // so 3 separate partitions with crossing cost = 3 > K=0. // Can't merge either since weight > B=1. let source = AcyclicPartition::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(), vec![1, 1, 1], vec![1, 1, 1], 1, 0, - ); + ) + .unwrap(); let reduction: ReductionAcyclicPartitionToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); - assert!(solver.solve(ilp).is_err()); + assert_eq!( + solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] @@ -96,12 +101,13 @@ fn test_acyclicpartition_to_ilp_bf_vs_ilp() { #[test] fn test_acyclicpartition_to_ilp_regression_direct_topological_labels() { let source = AcyclicPartition::new( - DirectedGraph::new(6, vec![(2, 1), (1, 0), (4, 3), (3, 2), (5, 4)]), + DirectedGraph::new(6, vec![(2, 1), (1, 0), (4, 3), (3, 2), (5, 4)]).unwrap(), vec![8, 3, 1, 9, 4, 4], vec![4, 10, 0, 7, 3], 11, 10, - ); + ) + .unwrap(); let reduction: ReductionAcyclicPartitionToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() diff --git a/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs b/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs index f044cf0f9..f516798db 100644 --- a/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs +++ b/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs @@ -11,7 +11,7 @@ fn small_instance() -> BalancedCompleteBipartiteSubgraph { // Edges: (0,0),(0,1),(1,0),(1,1),(2,1),(2,2) // K_{2,2} subgraph: L={0,1}, R={0,1} BalancedCompleteBipartiteSubgraph::new( - BipartiteGraph::new(3, 3, vec![(0, 0), (0, 1), (1, 0), (1, 1), (2, 1), (2, 2)]), + BipartiteGraph::new(3, 3, vec![(0, 0), (0, 1), (1, 0), (1, 1), (2, 1), (2, 2)]).unwrap(), 2, ) } @@ -38,14 +38,17 @@ fn test_reduction_shape() { fn test_infeasible_instance() { // No K_{3,3}: not all edges present let source = BalancedCompleteBipartiteSubgraph::new( - BipartiteGraph::new(3, 3, vec![(0, 0), (0, 1), (1, 0), (1, 1)]), + BipartiteGraph::new(3, 3, vec![(0, 0), (0, 1), (1, 0), (1, 1)]).unwrap(), 3, ); let reduction: ReductionBCBSToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = crate::solvers::ILPSolver::new(); - assert!(solver.solve(ilp).is_err()); + assert_eq!( + solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/bicliquecover_bmf.rs b/src/unit_tests/rules/bicliquecover_bmf.rs index 739b63df8..95216ef99 100644 --- a/src/unit_tests/rules/bicliquecover_bmf.rs +++ b/src/unit_tests/rules/bicliquecover_bmf.rs @@ -9,7 +9,7 @@ use crate::traits::Problem; #[test] fn test_bicliquecover_to_bmf_structure() { // Graph with edges (0,0) and (1,1), k=2 → BMF target is 2x2 identity, rank 2. - let problem = BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0), (1, 1)]), 2); + let problem = BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0), (1, 1)]).unwrap(), 2); let reduction: ReductionBicliqueCoverToBMF = ReduceTo::::reduce_to(&problem).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -21,7 +21,10 @@ fn test_bicliquecover_to_bmf_structure() { #[test] fn test_bicliquecover_to_bmf_overhead_matches_target_shape() { - let problem = BicliqueCover::new(BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (1, 2)]), 2); + let problem = BicliqueCover::new( + BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (1, 2)]).unwrap(), + 2, + ); let reduction: ReductionBicliqueCoverToBMF = ReduceTo::::reduce_to(&problem).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -56,7 +59,7 @@ fn test_bicliquecover_to_bmf_overhead_matches_target_shape() { fn test_bicliquecover_to_bmf_closed_loop_full_biclique() { // K_{2,2} at rank 1 — single biclique covers all 4 edges. let problem = BicliqueCover::new( - BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]), + BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]).unwrap(), 1, ); let reduction: ReductionBicliqueCoverToBMF = @@ -77,7 +80,7 @@ fn test_bicliquecover_to_bmf_closed_loop_full_biclique() { #[test] fn test_bicliquecover_to_bmf_closed_loop_identity_rank2() { // Identity-biadjacency at rank 2 — exact factorization needs two singleton bicliques. - let problem = BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0), (1, 1)]), 2); + let problem = BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0), (1, 1)]).unwrap(), 2); let reduction: ReductionBicliqueCoverToBMF = ReduceTo::::reduce_to(&problem).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -96,7 +99,7 @@ fn test_bicliquecover_to_bmf_closed_loop_identity_rank2() { #[test] fn test_bicliquecover_to_bmf_insufficient_rank() { // Identity biadjacency at rank 1 — infeasible for both problems. - let problem = BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0), (1, 1)]), 1); + let problem = BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0), (1, 1)]).unwrap(), 1); let reduction: ReductionBicliqueCoverToBMF = ReduceTo::::reduce_to(&problem).expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs b/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs index 249dabc98..1a0a0696a 100644 --- a/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs +++ b/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs @@ -9,10 +9,11 @@ use crate::traits::Problem; fn small_instance() -> BiconnectivityAugmentation { // Path 0-1-2-3, candidates: (0,2,1),(0,3,2),(1,3,1), budget=3 BiconnectivityAugmentation::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![(0, 2, 1), (0, 3, 2), (1, 3, 1)], 3, ) + .unwrap() } #[test] @@ -53,7 +54,8 @@ fn test_extract_solution() { #[test] fn test_trivial_single_vertex() { - let source = BiconnectivityAugmentation::new(SimpleGraph::new(1, vec![]), vec![], 0); + let source = + BiconnectivityAugmentation::new(SimpleGraph::new(1, vec![]).unwrap(), vec![], 0).unwrap(); let reduction: ReductionBiconnAugToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -67,10 +69,11 @@ fn test_trivial_single_vertex() { fn test_already_biconnected() { // Triangle is already biconnected let source = BiconnectivityAugmentation::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![], 0, - ); + ) + .unwrap(); let reduction: ReductionBiconnAugToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -96,14 +99,15 @@ fn test_biconnectivityaugmentation_to_ilp_all_two_vertex_instances() { for weight in [-2, 0, 2] { for budget in [-3, -1, 0, 1, 3] { let source = BiconnectivityAugmentation::new( - SimpleGraph::new(2, if base_edge { vec![(0, 1)] } else { vec![] }), + SimpleGraph::new(2, if base_edge { vec![(0, 1)] } else { vec![] }).unwrap(), if base_edge { vec![] } else { vec![(0, 1, weight)] }, budget, - ); + ) + .unwrap(); let reduction: ReductionBiconnAugToILP = ReduceTo::>::reduce_to(&source).unwrap(); let expected = BruteForce::new().solve(&source).unwrap().is_some(); @@ -130,7 +134,8 @@ fn test_biconnectivityaugmentation_to_ilp_empty_negative_budget() { for n in 0..=1 { for budget in [-1, 0, 1] { let source = - BiconnectivityAugmentation::<_, i64>::new(SimpleGraph::empty(n), vec![], budget); + BiconnectivityAugmentation::<_, i64>::new(SimpleGraph::empty(n), vec![], budget) + .unwrap(); let reduction: ReductionBiconnAugToILP = ReduceTo::>::reduce_to(&source).unwrap(); assert_eq!( @@ -142,7 +147,9 @@ fn test_biconnectivityaugmentation_to_ilp_empty_negative_budget() { .is_some(), budget >= 0 ); - assert_eq!(reduction.extract_solution(&vec![]).is_ok(), budget >= 0); + if budget >= 0 { + assert!(reduction.extract_solution(&vec![]).unwrap().is_empty()); + } } } } @@ -150,7 +157,7 @@ fn test_biconnectivityaugmentation_to_ilp_empty_negative_budget() { #[test] fn test_biconnectivityaugmentation_to_ilp_signed_cost_and_certificate_bounds() { for candidates in [vec![(0, 2, 2), (0, 3, -2)], vec![(0, 3, -2), (0, 2, 2)]] { - let source = BiconnectivityAugmentation::new(SimpleGraph::path(4), candidates, 0); + let source = BiconnectivityAugmentation::new(SimpleGraph::path(4), candidates, 0).unwrap(); let reduction: ReductionBiconnAugToILP = ReduceTo::>::reduce_to(&source).unwrap(); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); let z = ILPSolver::new().solve(reduction.target_problem()).unwrap(); @@ -160,12 +167,18 @@ fn test_biconnectivityaugmentation_to_ilp_signed_cost_and_certificate_bounds() { .unwrap() .0 ); - assert!(reduction.extract_solution(&vec![0; z.len()]).is_err()); - assert!(reduction.extract_solution(&vec![1; z.len() + 1]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &vec![0; z.len()]), Ok(value) if value.is_valid()) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &vec![1; z.len() + 1]), Ok(value) if value.is_valid()) + ); for value in [-1, 2] { let mut bad = z.clone(); bad[0] = value; - assert!(reduction.extract_solution(&bad).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &bad), Ok(value) if value.is_valid()) + ); } } } diff --git a/src/unit_tests/rules/bmf_bicliquecover.rs b/src/unit_tests/rules/bmf_bicliquecover.rs index 6ed1e3a2b..00c2795d4 100644 --- a/src/unit_tests/rules/bmf_bicliquecover.rs +++ b/src/unit_tests/rules/bmf_bicliquecover.rs @@ -8,7 +8,7 @@ use crate::traits::Problem; #[test] fn test_bmf_to_bicliquecover_structure() { // Matrix A = [[1,0],[0,1]] => bipartite graph with edges (0,0), (1,1). - let problem = BMF::new(vec![vec![true, false], vec![false, true]], 2); + let problem = BMF::new(vec![vec![true, false], vec![false, true]], 2).unwrap(); let reduction: ReductionBMFToBicliqueCover = ReduceTo::::reduce_to(&problem).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -21,7 +21,7 @@ fn test_bmf_to_bicliquecover_structure() { #[test] fn test_bmf_to_bicliquecover_closed_loop_all_ones() { // All-ones 2x2 at rank 1 — exact factorization exists. - let problem = BMF::new(vec![vec![true, true], vec![true, true]], 1); + let problem = BMF::new(vec![vec![true, true], vec![true, true]], 1).unwrap(); let reduction: ReductionBMFToBicliqueCover = ReduceTo::::reduce_to(&problem).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -42,7 +42,7 @@ fn test_bmf_to_bicliquecover_closed_loop_all_ones() { #[test] fn test_bmf_to_bicliquecover_closed_loop_identity() { // 2x2 identity at rank 2 — exact factorization exists. - let problem = BMF::new(vec![vec![true, false], vec![false, true]], 2); + let problem = BMF::new(vec![vec![true, false], vec![false, true]], 2).unwrap(); let reduction: ReductionBMFToBicliqueCover = ReduceTo::::reduce_to(&problem).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -66,7 +66,7 @@ fn test_bmf_to_bicliquecover_insufficient_rank() { // sub-biclique semantics a single biclique covering both (0,0) and (1,1) // would have to be the full K_{2,2}, which requires edges (0,1) and (1,0) // that are not in G. So BicliqueCover is infeasible too, matching BMF. - let problem = BMF::new(vec![vec![true, false], vec![false, true]], 1); + let problem = BMF::new(vec![vec![true, false], vec![false, true]], 1).unwrap(); let reduction: ReductionBMFToBicliqueCover = ReduceTo::::reduce_to(&problem).expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/unit_tests/rules/bmf_ilp.rs b/src/unit_tests/rules/bmf_ilp.rs index d09a728fe..8729b38b6 100644 --- a/src/unit_tests/rules/bmf_ilp.rs +++ b/src/unit_tests/rules/bmf_ilp.rs @@ -6,7 +6,7 @@ use crate::rules::{ReduceTo, ReductionResult}; #[test] fn test_bmf_to_ilp_structure() { // 2x2 identity matrix, rank 1 - let problem = BMF::new(vec![vec![true, false], vec![false, true]], 1); + let problem = BMF::new(vec![vec![true, false], vec![false, true]], 1).unwrap(); let reduction: ReductionBMFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -19,7 +19,7 @@ fn test_bmf_to_ilp_structure() { fn test_bmf_to_ilp_closed_loop() { // 2x2 identity, rank 2 — exact factorization exists. // Use ILP solver on target (fast) + brute force on source (tiny 2x2). - let problem = BMF::new(vec![vec![true, false], vec![false, true]], 2); + let problem = BMF::new(vec![vec![true, false], vec![false, true]], 2).unwrap(); let reduction: ReductionBMFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_bf_vs_ilp(&problem, &reduction); @@ -28,7 +28,7 @@ fn test_bmf_to_ilp_closed_loop() { #[test] fn test_bmf_to_ilp_bf_vs_ilp() { // All-ones 2x2 has an exact rank-1 factorization (boolean rank 1). - let problem = BMF::new(vec![vec![true, true], vec![true, true]], 1); + let problem = BMF::new(vec![vec![true, true], vec![true, true]], 1).unwrap(); let reduction: ReductionBMFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_bf_vs_ilp(&problem, &reduction); @@ -37,7 +37,7 @@ fn test_bmf_to_ilp_bf_vs_ilp() { #[test] fn test_bmf_to_ilp_trivial() { // 1x1 matrix, rank 1 - let problem = BMF::new(vec![vec![true]], 1); + let problem = BMF::new(vec![vec![true]], 1).unwrap(); let reduction: ReductionBMFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs b/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs index 5d92bc4bb..bef0e10b8 100644 --- a/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs @@ -5,9 +5,10 @@ use crate::traits::Problem; fn k4_btsp() -> BottleneckTravelingSalesman { BottleneckTravelingSalesman::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), vec![1, 3, 2, 4, 2, 1], ) + .unwrap() } #[test] @@ -51,9 +52,10 @@ fn test_bottlenecktravelingsalesman_to_ilp_closed_loop() { fn test_bottlenecktravelingsalesman_to_ilp_c4() { // C4 with varying weights: bottleneck = max weight in the only cycle let problem = BottleneckTravelingSalesman::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(), vec![1, 2, 3, 4], - ); + ) + .unwrap(); let bf = BruteForce::new(); let bf_solution = bf.solve(&problem).unwrap().expect("brute-force optimum"); let bf_value = problem.evaluate(&bf_solution).unwrap(); @@ -89,15 +91,17 @@ fn test_solution_extraction() { fn test_no_hamiltonian_cycle_infeasible() { // Path graph: no Hamiltonian cycle let problem = BottleneckTravelingSalesman::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1, 1, 1], - ); + ) + .unwrap(); let reduction: ReductionBTSPToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(reduction.target_problem()); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Path graph should have no Hamiltonian cycle" ); } @@ -168,7 +172,8 @@ fn test_bottleneck_ilp_signed_full_range_and_native_cycles() { vec![1, 2, 3], ), ] { - let source = BottleneckTravelingSalesman::new(SimpleGraph::new(n, edges), weights); + let source = + BottleneckTravelingSalesman::new(SimpleGraph::new(n, edges).unwrap(), weights).unwrap(); let result = ReduceTo::>::reduce_to(&source).unwrap(); let witness = tour_witness(&source, &tour, &edge_order); let extracted = result.extract_solution(&witness).unwrap(); @@ -181,11 +186,13 @@ fn test_bottleneck_ilp_signed_full_range_and_native_cycles() { for variable in 0..witness.len() { let mut invalid = witness.clone(); invalid[variable] = 2; - assert!(result.extract_solution(&invalid).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(result.target_problem(), &invalid), Ok(value) if value.is_valid()) + ); } - assert!(result - .extract_solution(&witness[..witness.len() - 1].to_vec()) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(result.target_problem(), &witness[..witness.len() - 1].to_vec()), Ok(value) if value.is_valid()) + ); } } @@ -196,18 +203,25 @@ fn test_bottleneck_ilp_maximum_must_be_used_and_dominate() { let mut config = tour_witness(&source, &[0, 1, 2, 3], &[0, 3, 5, 2]); let selector = 4 * 4 + 2 * 6 * 4; config[selector..].fill(0); - assert!(result.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(result.target_problem(), &config), Ok(value) if value.is_valid()) + ); config[selector] = 1; // used, but lower than the maximum edge - assert!(result.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(result.target_problem(), &config), Ok(value) if value.is_valid()) + ); config[selector] = 0; config[selector + 1] = 1; // unused - assert!(result.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(result.target_problem(), &config), Ok(value) if value.is_valid()) + ); } #[test] fn test_bottleneck_ilp_empty_and_single_edge_are_infeasible() { for (n, edges, weights) in [(0, vec![], vec![]), (2, vec![(0, 1)], vec![1])] { - let source = BottleneckTravelingSalesman::new(SimpleGraph::new(n, edges), weights); + let source = + BottleneckTravelingSalesman::new(SimpleGraph::new(n, edges).unwrap(), weights).unwrap(); let result = ReduceTo::>::reduce_to(&source).unwrap(); assert!(matches!( ILPSolver::new().solve(result.target_problem()), @@ -226,9 +240,10 @@ fn test_bottleneck_ilp_dimensions_and_malformed_weights() { for (n, m) in [(usize::MAX, 0), (1, usize::MAX), (0, usize::MAX)] { assert!(ReductionBTSPToILP::dimensions(n, m).is_err()); } - let source: BottleneckTravelingSalesman = serde_json::from_value(serde_json::json!({ - "graph": {"num_vertices": 0, "edges": []}, "edge_weights": [1] - })) - .unwrap(); - assert!(ReduceTo::>::reduce_to(&source).is_err()); + assert!( + serde_json::from_value::(serde_json::json!({ + "graph": {"num_vertices": 0, "edges": []}, "edge_weights": [1] + })) + .is_err() + ); } diff --git a/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs b/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs index 039d7f2c3..1cb59b798 100644 --- a/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs @@ -9,11 +9,12 @@ use crate::traits::Problem; fn small_instance() -> BoundedComponentSpanningForest { // Path 0-1-2-3, weights [1,2,2,1], K=2, B=4 BoundedComponentSpanningForest::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1, 2, 2, 1], 2, 4, ) + .unwrap() } #[test] @@ -56,11 +57,12 @@ fn test_extract_solution() { fn test_single_component() { // All in one component let source = BoundedComponentSpanningForest::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1, 1, 1], 1, 3, - ); + ) + .unwrap(); let reduction: ReductionBCSFToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -76,16 +78,20 @@ fn test_single_component() { fn test_infeasible_instance() { // 4 vertices, weights [3,3,3,3], K=2, B=5 -> total weight 12, max per component 5, need at least 3 components let source = BoundedComponentSpanningForest::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![3, 3, 3, 3], 2, 5, - ); + ) + .unwrap(); let reduction: ReductionBCSFToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); - assert!(solver.solve(ilp).is_err()); + assert_eq!( + solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/capacityassignment_ilp.rs b/src/unit_tests/rules/capacityassignment_ilp.rs index e0053dfb1..540fb3474 100644 --- a/src/unit_tests/rules/capacityassignment_ilp.rs +++ b/src/unit_tests/rules/capacityassignment_ilp.rs @@ -11,7 +11,8 @@ fn test_reduction_creates_valid_ilp() { vec![vec![1, 3], vec![2, 4]], vec![vec![8, 4], vec![7, 3]], 12, - ); + ) + .unwrap(); let reduction: ReductionCAToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -49,7 +50,8 @@ fn test_capacityassignment_to_ilp_closed_loop() { vec![vec![1, 3, 6], vec![2, 4, 7], vec![1, 2, 5]], vec![vec![8, 4, 1], vec![7, 3, 1], vec![6, 3, 1]], 12, - ); + ) + .unwrap(); let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); @@ -81,23 +83,22 @@ fn test_solution_extraction() { vec![vec![1, 3, 6], vec![2, 4, 7]], vec![vec![8, 4, 1], vec![7, 3, 1]], 10, - ); + ) + .unwrap(); let reduction: ReductionCAToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - // link 0 → cap 1, link 1 → cap 0 - // x_{0,0}=0, x_{0,1}=1, x_{0,2}=0, x_{1,0}=1, x_{1,1}=0, x_{1,2}=0 - let ilp_solution = vec![0, 1, 0, 1, 0, 0]; + // Both links choose capacity level 1: total delay 4 + 3 <= 10. + let ilp_solution = vec![0, 1, 0, 0, 1, 0]; let extracted = reduction.extract_solution(&ilp_solution).unwrap(); - assert_eq!(extracted, vec![1, 0]); - // Verify extraction works (evaluation may or may not be feasible) - let _ = problem.evaluate(&extracted).unwrap(); + assert_eq!(extracted, vec![1, 1]); + assert!(problem.evaluate(&extracted).unwrap().is_valid()); } #[test] fn test_capacityassignment_to_ilp_trivial() { // 1 link, 1 capacity level — trivially feasible - let problem = CapacityAssignment::new(vec![1], vec![vec![0]], vec![vec![0]], 100); + let problem = CapacityAssignment::new(vec![1], vec![vec![0]], vec![vec![0]], 100).unwrap(); let reduction: ReductionCAToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -119,7 +120,8 @@ fn test_capacityassignment_to_ilp_bf_vs_ilp() { vec![vec![1, 3, 6], vec![2, 4, 7], vec![1, 2, 5]], vec![vec![8, 4, 1], vec![7, 3, 1], vec![6, 3, 1]], 12, - ); + ) + .unwrap(); let reduction: ReductionCAToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); diff --git a/src/unit_tests/rules/circuit_ilp.rs b/src/unit_tests/rules/circuit_ilp.rs index 044e10148..ea48a23ba 100644 --- a/src/unit_tests/rules/circuit_ilp.rs +++ b/src/unit_tests/rules/circuit_ilp.rs @@ -144,7 +144,9 @@ fn test_circuit_ilp_native_folds_all_feasible_witnesses() { assert!(source.evaluate(&extracted).unwrap().0); actual.insert(extracted); } else { - assert!(reduction.extract_solution(&solution).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &solution), Ok(value) if value.is_valid()) + ); } } assert_eq!(actual, expected, "{expr:?}, output={output}"); @@ -161,7 +163,9 @@ fn test_circuit_ilp_rejects_invalid_target_and_supports_empty_circuit() { )])); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); for invalid in [vec![], vec![0], vec![0, 0], vec![2, 1], vec![1, 1, 1]] { - assert!(reduction.extract_solution(&invalid).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &invalid), Ok(value) if value.is_valid()) + ); } let empty = CircuitSAT::new(Circuit::new(vec![])); let reduction = ReduceTo::>::reduce_to(&empty).unwrap(); diff --git a/src/unit_tests/rules/circuit_spinglass.rs b/src/unit_tests/rules/circuit_spinglass.rs index d44aaec9d..08cb4e8c3 100644 --- a/src/unit_tests/rules/circuit_spinglass.rs +++ b/src/unit_tests/rules/circuit_spinglass.rs @@ -5,7 +5,6 @@ use crate::solvers::BruteForce; use crate::traits::Problem; use crate::types::{NumericSize, WeightElement}; use num_traits::Num; -include!("../jl_helpers.rs"); /// Verify a gadget has the correct ground states. fn verify_gadget_truth_table(gadget: &LogicGadget, expected: &[(Vec, Vec)]) @@ -359,7 +358,9 @@ fn test_circuit_spinglass_all_threshold_witnesses_native_domain() { assert!(source.evaluate(&decoded).unwrap().0); actual.insert(decoded); } else { - assert!(reduction.extract_solution(&spins).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &spins), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } assert_eq!(actual, expected, "expression {expr:?}, output {output}"); @@ -382,10 +383,14 @@ fn test_circuit_spinglass_unsat_threshold_and_invalid_spins() { .find_all_witnesses(ReductionResult::target_problem(&reduction)) .unwrap() { - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } for bad in [vec![], vec![1], vec![0, 0], vec![1, 1, 1]] { - assert!(reduction.extract_solution(&bad).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &bad), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } let empty = CircuitSAT::new(Circuit::new(vec![])); let reduction = ReduceTo::>::reduce_to(&empty).unwrap(); diff --git a/src/unit_tests/rules/closeststring_ilp.rs b/src/unit_tests/rules/closeststring_ilp.rs index 5714565a7..b04599c0b 100644 --- a/src/unit_tests/rules/closeststring_ilp.rs +++ b/src/unit_tests/rules/closeststring_ilp.rs @@ -14,6 +14,7 @@ fn issue_instance() -> ClosestString { 2, vec![vec![0, 0, 0], vec![0, 1, 1], vec![1, 0, 1], vec![1, 1, 0]], ) + .unwrap() } #[test] @@ -96,16 +97,14 @@ fn test_closeststring_to_ilp_extract_known_center() { #[test] fn test_closeststring_to_ilp_rejects_missing_one_hot_symbol() { - let source = ClosestString::new(2, vec![vec![0, 1]]); + let source = ClosestString::new(2, vec![vec![0, 1]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target_solution = vec![0; reduction.target_problem().num_vars()]; - assert_eq!( - reduction - .extract_solution(&target_solution) - .unwrap_err() - .to_string(), - "center position 0 has no selected symbol" + assert!( + !crate::traits::Problem::evaluate(reduction.target_problem(), &target_solution) + .unwrap() + .is_valid() ); } @@ -113,7 +112,7 @@ fn test_closeststring_to_ilp_rejects_missing_one_hot_symbol() { fn test_closeststring_to_ilp_ternary_alphabet() { // q = 3, m = 2, three strings forcing a nonzero radius. The optimum // radius is 1 (any center matches at least one position of every string). - let source = ClosestString::new(3, vec![vec![0, 1], vec![1, 2], vec![2, 0]]); + let source = ClosestString::new(3, vec![vec![0, 1], vec![1, 2], vec![2, 0]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -129,7 +128,7 @@ fn test_closeststring_to_ilp_single_string_zero_radius() { // A single input string: the center equals the input and the optimum // radius is 0. This guards against off-by-one errors in the radius // constraints. - let source = ClosestString::new(2, vec![vec![1, 0, 1, 1]]); + let source = ClosestString::new(2, vec![vec![1, 0, 1, 1]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() diff --git a/src/unit_tests/rules/closestsubstring_ilp.rs b/src/unit_tests/rules/closestsubstring_ilp.rs index bf3a9fed5..5797cb02d 100644 --- a/src/unit_tests/rules/closestsubstring_ilp.rs +++ b/src/unit_tests/rules/closestsubstring_ilp.rs @@ -77,12 +77,10 @@ fn test_closestsubstring_to_ilp_rejects_missing_one_hot_symbol() { let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target_solution = vec![0; reduction.target_problem().num_vars()]; - assert_eq!( - reduction - .extract_solution(&target_solution) - .unwrap_err() - .to_string(), - "center position 0 has no selected value" + assert!( + !crate::traits::Problem::evaluate(reduction.target_problem(), &target_solution) + .unwrap() + .is_valid() ); } diff --git a/src/unit_tests/rules/closestvectorproblem_qubo.rs b/src/unit_tests/rules/closestvectorproblem_qubo.rs index aa4afb7b3..22de016f9 100644 --- a/src/unit_tests/rules/closestvectorproblem_qubo.rs +++ b/src/unit_tests/rules/closestvectorproblem_qubo.rs @@ -59,7 +59,10 @@ fn test_closestvectorproblem_to_qubo_twelve_dimensional_identity() { } let solution = reduction.extract_solution(&bits).unwrap(); assert_eq!(solution, vec![1; size]); - assert_eq!(source.evaluate(&solution).unwrap().0, Some(0.0)); + assert_eq!( + source.evaluate(&solution).unwrap().0, + Some(num_rational::BigRational::zero()) + ); } #[test] @@ -73,7 +76,10 @@ fn test_closestvectorproblem_to_qubo_closed_loop() { let source_solution = reduction.extract_solution(&target_solution).unwrap(); assert_eq!(source_solution, vec![1, 1]); - assert_eq!(source.evaluate(&source_solution).unwrap().0, Some(0.0)); + assert_eq!( + source.evaluate(&source_solution).unwrap().0, + Some(num_rational::BigRational::zero()) + ); assert_eq!(reduction.target_problem().num_vars(), 11); } @@ -82,10 +88,10 @@ fn test_closestvectorproblem_to_qubo_coefficients() { let reduction = ReduceTo::>::reduce_to(&canonical_cvp()).unwrap(); let qubo = reduction.target_problem(); - assert_eq!(qubo.get(0, 0), Some(&-248)); - assert_eq!(qubo.get(0, 1), Some(&16)); - assert_eq!(qubo.get(0, 6), Some(&4)); - assert_eq!(qubo.get(6, 6), Some(&-241)); + assert_eq!(qubo.get(0, 0), Some(-248)); + assert_eq!(qubo.get(0, 1), Some(16)); + assert_eq!(qubo.get(0, 6), Some(4)); + assert_eq!(qubo.get(6, 6), Some(-241)); } #[test] @@ -146,7 +152,7 @@ fn test_closestvectorproblem_to_qubo_canonical_example_spec() { assert_eq!(example.source.problem, "ClosestVectorProblem"); assert_eq!(example.target.problem, "QUBO"); - assert_eq!(example.target.instance["num_vars"], 11); + assert_eq!(example.target.instance["matrix"]["nrows"], 11); assert_eq!( example.solutions[0].source_config, serde_json::json!([1, 1]) @@ -156,3 +162,21 @@ fn test_closestvectorproblem_to_qubo_canonical_example_spec() { serde_json::to_value(canonical_bits()).unwrap() ); } + +#[test] +fn qubo_energy_matches_squared_distance_up_to_the_dropped_constant() { + let source = ClosestVectorProblem::new(vec![vec![2]], vec![1_i64]).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = reduction.target_problem(); + assert_eq!(target.num_vars(), 3); + // The all-zero encoding represents x=-2, with squared distance (-4-1)^2=25. + for index in 0..8 { + let bits = (0..3).map(|bit| index & (1 << bit) != 0).collect(); + let coefficient = reduction.extract_solution(&bits).unwrap(); + let energy = target.evaluate(&bits).unwrap().unwrap(); + assert_eq!( + source.squared_distance(&coefficient).unwrap(), + num_rational::BigRational::from_integer((energy + 25).into()) + ); + } +} diff --git a/src/unit_tests/rules/clustering_ilp.rs b/src/unit_tests/rules/clustering_ilp.rs index 2142986ba..16d1539f1 100644 --- a/src/unit_tests/rules/clustering_ilp.rs +++ b/src/unit_tests/rules/clustering_ilp.rs @@ -16,10 +16,11 @@ fn canonical_yes_instance() -> Clustering { 2, 1, ) + .unwrap() } fn infeasible_instance() -> Clustering { - Clustering::new(vec![vec![0, 3, 1], vec![3, 0, 1], vec![1, 1, 0]], 1, 1) + Clustering::new(vec![vec![0, 3, 1], vec![3, 0, 1], vec![1, 1, 0]], 1, 1).unwrap() } #[test] @@ -76,5 +77,8 @@ fn test_clustering_to_ilp_infeasible_instance_is_infeasible() { let reduction: ReductionClusteringToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } diff --git a/src/unit_tests/rules/coloring_ilp.rs b/src/unit_tests/rules/coloring_ilp.rs index 238d2cbaf..548c71df5 100644 --- a/src/unit_tests/rules/coloring_ilp.rs +++ b/src/unit_tests/rules/coloring_ilp.rs @@ -6,7 +6,8 @@ use crate::variant::{K1, K2, K3, K4, KN}; #[test] fn test_reduction_creates_valid_ilp() { // Triangle graph with 3 colors - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let problem = + KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -33,7 +34,7 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_reduction_path_graph() { // Path graph 0-1-2 with 2 colors (2-colorable) - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -46,7 +47,7 @@ fn test_reduction_path_graph() { #[test] fn runtime_color_count_controls_exact_ilp_parameters() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); for colors in [2, 3, 5] { let problem = KColoring::::with_k(graph.clone(), colors); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); @@ -60,7 +61,8 @@ fn runtime_color_count_controls_exact_ilp_parameters() { #[test] fn test_coloring_to_ilp_closed_loop() { // Triangle needs 3 colors - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let problem = + KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -93,7 +95,8 @@ fn test_coloring_to_ilp_closed_loop() { #[test] fn test_ilp_solution_equals_brute_force_path() { // Path graph 0-1-2-3 with 2 colors - let problem = KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = + KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -118,7 +121,8 @@ fn test_ilp_solution_equals_brute_force_path() { #[test] fn test_ilp_infeasible_triangle_2_colors() { // Triangle cannot be 2-colored - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let problem = + KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -126,15 +130,16 @@ fn test_ilp_infeasible_triangle_2_colors() { // ILP should be infeasible let result = ilp_solver.solve(ilp); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Triangle with 2 colors should be infeasible" ); } #[test] fn test_solution_extraction() { - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1)])); + let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // ILP solution where: @@ -154,7 +159,7 @@ fn test_solution_extraction() { #[test] fn test_ilp_structure() { let problem = - KColoring::::new(SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)])); + KColoring::::new(SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -167,7 +172,7 @@ fn test_ilp_structure() { #[test] fn test_empty_graph() { // Graph with no edges: any coloring is valid - let problem = KColoring::::new(SimpleGraph::new(3, vec![])); + let problem = KColoring::::new(SimpleGraph::new(3, vec![]).unwrap()); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -184,10 +189,9 @@ fn test_empty_graph() { #[test] fn test_complete_graph_k4() { // K4 needs 4 colors - let problem = KColoring::::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )); + let problem = KColoring::::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), + ); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -207,16 +211,19 @@ fn test_complete_graph_k4() { #[test] fn test_complete_graph_k4_with_3_colors_infeasible() { // K4 cannot be 3-colored - let problem = KColoring::::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )); + let problem = KColoring::::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), + ); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(ilp); - assert!(result.is_err(), "K4 with 3 colors should be infeasible"); + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), + "K4 with 3 colors should be infeasible" + ); } #[test] @@ -224,7 +231,7 @@ fn test_bipartite_graph() { // Complete bipartite K_{2,2}: 0-2, 0-3, 1-2, 1-3 // This is 2-colorable let problem = - KColoring::::new(SimpleGraph::new(4, vec![(0, 2), (0, 3), (1, 2), (1, 3)])); + KColoring::::new(SimpleGraph::new(4, vec![(0, 2), (0, 3), (1, 2), (1, 3)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -243,7 +250,8 @@ fn test_bipartite_graph() { #[test] fn test_reduction_closed_loop() { - let problem = KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = + KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let target_solution = ILPSolver::new() .solve(reduction.target_problem()) @@ -256,7 +264,7 @@ fn test_reduction_closed_loop() { #[test] fn test_single_vertex() { // Single vertex graph: always 1-colorable - let problem = KColoring::::new(SimpleGraph::new(1, vec![])); + let problem = KColoring::::new(SimpleGraph::new(1, vec![]).unwrap()); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -273,7 +281,7 @@ fn test_single_vertex() { #[test] fn test_single_edge() { // Single edge: needs 2 colors - let problem = KColoring::::new(SimpleGraph::new(2, vec![(0, 1)])); + let problem = KColoring::::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -287,7 +295,8 @@ fn test_single_edge() { #[test] fn test_coloring_to_ilp_bf_vs_ilp() { - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let problem = + KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } diff --git a/src/unit_tests/rules/coloring_qubo.rs b/src/unit_tests/rules/coloring_qubo.rs index 2b4ea3793..5441be970 100644 --- a/src/unit_tests/rules/coloring_qubo.rs +++ b/src/unit_tests/rules/coloring_qubo.rs @@ -7,7 +7,7 @@ use crate::variant::{K2, K3}; #[test] fn test_kcoloring_to_qubo_closed_loop() { // Triangle K3, 3 colors → exactly 6 valid colorings (3! permutations) - let kc = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let kc = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&kc).expect("reduction should succeed"); let qubo = reduction.target_problem(); @@ -27,7 +27,7 @@ fn test_kcoloring_to_qubo_closed_loop() { #[test] fn test_kcoloring_to_qubo_path() { // Path graph: 0-1-2, 2 colors - let kc = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let kc = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&kc).expect("reduction should succeed"); let qubo = reduction.target_problem(); @@ -47,7 +47,7 @@ fn test_kcoloring_to_qubo_path() { fn test_kcoloring_to_qubo_reversed_edges() { // Edge (2, 0) triggers the idx_v < idx_u swap branch (line 104). // Path: 2-0-1 with reversed edge ordering - let kc = KColoring::::new(SimpleGraph::new(3, vec![(2, 0), (0, 1)])); + let kc = KColoring::::new(SimpleGraph::new(3, vec![(2, 0), (0, 1)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&kc).expect("reduction should succeed"); let qubo = reduction.target_problem(); @@ -65,11 +65,11 @@ fn test_kcoloring_to_qubo_reversed_edges() { #[test] fn test_kcoloring_to_qubo_sizes() { - let kc = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let kc = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&kc).expect("reduction should succeed"); // QUBO should have n*K = 3*3 = 9 variables - assert_eq!(reduction.target_problem().num_variables(), 9); + assert_eq!(reduction.target_problem().num_variables().unwrap(), 9); } #[test] @@ -86,7 +86,8 @@ fn test_kcoloring_to_qubo_all_small_graphs_and_configurations() { .filter_map(|(i, &edge)| ((mask >> i) & 1 == 1).then_some(edge)) .collect(); for k in 0..=3 { - let source = KColoring::::with_k(SimpleGraph::new(n, edges.clone()), k); + let source = + KColoring::::with_k(SimpleGraph::new(n, edges.clone()).unwrap(), k); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = AggregateReductionResult::target_problem(&reduction); assert_eq!(target.num_vars(), n * k); @@ -116,13 +117,10 @@ fn test_kcoloring_to_qubo_all_small_graphs_and_configurations() { AggregateReductionResult::extract_value(&reduction, value).0, expected ); - match reduction.extract_solution(&config) { - Ok(coloring) => { - assert!(expected); - assert!(source.evaluate(&coloring).unwrap().0); - any_coloring = true; - } - Err(_) => assert!(!expected), + if expected { + let coloring = reduction.extract_solution(&config).unwrap(); + assert!(source.evaluate(&coloring).unwrap().0); + any_coloring = true; } } assert_eq!( @@ -136,7 +134,9 @@ fn test_kcoloring_to_qubo_all_small_graphs_and_configurations() { assert!( !AggregateReductionResult::extract_value(&reduction, crate::types::Min(None)).0 ); - assert!(reduction.extract_solution(&vec![false; n * k + 1]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; n * k + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } diff --git a/src/unit_tests/rules/consecutiveblockminimization_ilp.rs b/src/unit_tests/rules/consecutiveblockminimization_ilp.rs index f778a2f96..64a2edfc5 100644 --- a/src/unit_tests/rules/consecutiveblockminimization_ilp.rs +++ b/src/unit_tests/rules/consecutiveblockminimization_ilp.rs @@ -11,7 +11,8 @@ fn test_cbm_to_ilp_structure() { let problem = ConsecutiveBlockMinimization::new( vec![vec![true, false, true], vec![false, true, true]], 2, - ); + ) + .unwrap(); let reduction: ReductionCBMToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -25,7 +26,8 @@ fn test_cbm_to_ilp_closed_loop() { let problem = ConsecutiveBlockMinimization::new( vec![vec![true, false, true], vec![false, true, true]], 2, - ); + ) + .unwrap(); let reduction: ReductionCBMToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_bf_vs_ilp(&problem, &reduction); @@ -36,7 +38,8 @@ fn test_cbm_to_ilp_bf_vs_ilp() { let problem = ConsecutiveBlockMinimization::new( vec![vec![true, false, true], vec![false, true, true]], 2, - ); + ) + .unwrap(); let reduction: ReductionCBMToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -55,7 +58,7 @@ fn test_cbm_to_ilp_bf_vs_ilp() { #[test] fn test_cbm_to_ilp_trivial() { // 1x1 matrix, bound 1 - let problem = ConsecutiveBlockMinimization::new(vec![vec![true]], 1); + let problem = ConsecutiveBlockMinimization::new(vec![vec![true]], 1).unwrap(); let reduction: ReductionCBMToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/consecutiveonessubmatrix_ilp.rs b/src/unit_tests/rules/consecutiveonessubmatrix_ilp.rs index b6a7ecf2f..4b049c4d3 100644 --- a/src/unit_tests/rules/consecutiveonessubmatrix_ilp.rs +++ b/src/unit_tests/rules/consecutiveonessubmatrix_ilp.rs @@ -15,7 +15,8 @@ fn test_cos_to_ilp_structure() { vec![false, true, true, false], ], 3, - ); + ) + .unwrap(); let reduction: ReductionCOSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -34,7 +35,8 @@ fn test_cos_to_ilp_closed_loop() { vec![false, true, true, false], ], 3, - ); + ) + .unwrap(); let reduction: ReductionCOSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -61,7 +63,8 @@ fn test_cos_to_ilp_bf_vs_ilp() { vec![false, true, true, false], ], 3, - ); + ) + .unwrap(); let reduction: ReductionCOSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -86,7 +89,8 @@ fn test_cos_to_ilp_allows_zero_rows_in_selected_submatrix() { vec![true, false, false, true], ], 1, - ); + ) + .unwrap(); let reduction: ReductionCOSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -100,7 +104,8 @@ fn test_cos_to_ilp_allows_zero_rows_in_selected_submatrix() { #[test] fn test_cos_to_ilp_trivial() { // 2x2 identity, K=2 - let problem = ConsecutiveOnesSubmatrix::new(vec![vec![true, false], vec![false, true]], 2); + let problem = + ConsecutiveOnesSubmatrix::new(vec![vec![true, false], vec![false, true]], 2).unwrap(); let reduction: ReductionCOSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); diff --git a/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs b/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs index c9e07b866..56d16828d 100644 --- a/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs +++ b/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs @@ -13,6 +13,7 @@ fn small_yes_instance() -> ConsistencyOfDatabaseFrequencyTables { vec![FrequencyTable::new(0, 1, vec![vec![1, 0], vec![0, 1]])], vec![KnownValue::new(0, 0, 0)], ) + .unwrap() } fn small_yes_witness() -> Vec { @@ -26,6 +27,7 @@ fn small_no_instance() -> ConsistencyOfDatabaseFrequencyTables { vec![FrequencyTable::new(0, 1, vec![vec![1, 0], vec![0, 1]])], vec![KnownValue::new(0, 0, 0), KnownValue::new(1, 1, 0)], ) + .unwrap() } #[test] @@ -65,7 +67,10 @@ fn test_cdft_to_ilp_unsat_instance_is_infeasible() { let reduction: ReductionCDFTToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let solver = ILPSolver::new(); - assert!(solver.solve(reduction.target_problem()).is_err()); + assert_eq!( + solver.solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] @@ -111,6 +116,7 @@ fn issue_instance() -> ConsistencyOfDatabaseFrequencyTables { KnownValue::new(1, 2, 1), ], ) + .unwrap() } fn issue_witness() -> Vec { diff --git a/src/unit_tests/rules/decisionmaximumindependentset_integralflowbundles.rs b/src/unit_tests/rules/decisionmaximumindependentset_integralflowbundles.rs index 50c1b776d..0c49f0c6f 100644 --- a/src/unit_tests/rules/decisionmaximumindependentset_integralflowbundles.rs +++ b/src/unit_tests/rules/decisionmaximumindependentset_integralflowbundles.rs @@ -9,7 +9,7 @@ fn source( bound: i64, ) -> Decision> { Decision::new( - MaximumIndependentSet::new(SimpleGraph::new(n, edges), vec![One; n]), + MaximumIndependentSet::new(SimpleGraph::new(n, edges).unwrap(), vec![One; n]).unwrap(), bound, ) } @@ -102,7 +102,9 @@ fn test_decision_ifb_loops_parallel_edges_and_invalid_witnesses() { vec![1, 1, 0, 0, 1, 1, 1, 1], // self-loop vec![0, 0, 2, 2, 1, 1, 1, 1], // path capacity ] { - assert!(reduction.extract_solution(&flow).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &flow), Ok(value) if { value.is_valid() }) + ); } } @@ -170,7 +172,8 @@ fn test_decision_ifb_registration_replaces_optimization_edge() { // The dynamic executor must reject the old weighted source type, even though // it shares the same decision problem name with the registered unit variant. let weighted = Decision::new( - MaximumIndependentSet::new(SimpleGraph::new(0, vec![]), Vec::::new()), + MaximumIndependentSet::new(SimpleGraph::new(0, vec![]).unwrap(), Vec::::new()) + .unwrap(), 0, ); assert!(matches!( diff --git a/src/unit_tests/rules/decisionminimumdominatingset_minimumsummulticenter.rs b/src/unit_tests/rules/decisionminimumdominatingset_minimumsummulticenter.rs index 5f39c738d..cfbec68e3 100644 --- a/src/unit_tests/rules/decisionminimumdominatingset_minimumsummulticenter.rs +++ b/src/unit_tests/rules/decisionminimumdominatingset_minimumsummulticenter.rs @@ -13,9 +13,10 @@ fn decision_mds( ) -> Decision> { Decision::new( MinimumDominatingSet::new( - SimpleGraph::new(num_vertices, edges.to_vec()), + SimpleGraph::new(num_vertices, edges.to_vec()).unwrap(), vec![One; num_vertices], - ), + ) + .unwrap(), k, ) } @@ -101,7 +102,9 @@ fn test_decisionminimumdominatingset_to_minimumsummulticenter_closed_loop_no_ins ), Or(false) ); - assert!(reduction.extract_solution(&target_solution).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &target_solution), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } @@ -144,12 +147,9 @@ fn test_decisionminimumdominatingset_to_minimumsummulticenter_all_small_graphs() } let accepted = crate::rules::AggregateReductionResult::extract_value(&reduction, value).0; - match reduction.extract_solution(&placement) { - Ok(witness) => { - assert!(accepted); - assert_eq!(source.evaluate(&witness).unwrap(), Or(true)); - } - Err(_) => assert!(!accepted), + if accepted { + let witness = reduction.extract_solution(&placement).unwrap(); + assert_eq!(source.evaluate(&witness).unwrap(), Or(true)); } } assert_eq!( @@ -157,9 +157,9 @@ fn test_decisionminimumdominatingset_to_minimumsummulticenter_all_small_graphs() Or(source_yes), "n={n}, edges={edges:?}, K={bound}" ); - assert!(reduction - .extract_solution(&vec![false; target.num_vertices() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vertices() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } diff --git a/src/unit_tests/rules/decisionminimumdominatingset_minmaxmulticenter.rs b/src/unit_tests/rules/decisionminimumdominatingset_minmaxmulticenter.rs index 9e15b4e34..5e8f13b2b 100644 --- a/src/unit_tests/rules/decisionminimumdominatingset_minmaxmulticenter.rs +++ b/src/unit_tests/rules/decisionminimumdominatingset_minmaxmulticenter.rs @@ -8,7 +8,8 @@ fn decision_mds( bound: i64, ) -> Decision> { Decision::new( - MinimumDominatingSet::new(SimpleGraph::new(n, edges.to_vec()), vec![One; n]), + MinimumDominatingSet::new(SimpleGraph::new(n, edges.to_vec()).unwrap(), vec![One; n]) + .unwrap(), bound, ) } @@ -42,7 +43,9 @@ fn test_decisionminimumdominatingset_to_minmaxmulticenter_closed_loop() { crate::rules::AggregateReductionResult::extract_value(&reduction, optimum), Or(false) ); - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } #[test] @@ -97,7 +100,9 @@ fn test_multicenter_all_small_graphs_bounds_and_placements() { .0 ); } else { - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } assert_eq!( @@ -119,7 +124,9 @@ fn test_multicenter_duplicate_edges_and_malformed_witness() { vec![true, false, true] ); for bad in [vec![], vec![true; 6]] { - assert!(reduction.extract_solution(&bad).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &bad), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } assert_eq!( crate::rules::AggregateReductionResult::extract_value(&reduction, Min(None)), diff --git a/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs b/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs index 67ba7ee19..c82b1748a 100644 --- a/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs +++ b/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs @@ -9,21 +9,21 @@ use crate::traits::Problem; fn decision_mvc( num_vertices: usize, edges: &[(usize, usize)], - weights: &[i64], k: i64, -) -> Decision> { +) -> Decision> { Decision::new( MinimumVertexCover::new( - SimpleGraph::new(num_vertices, edges.to_vec()), - weights.to_vec(), - ), + SimpleGraph::new(num_vertices, edges.to_vec()).unwrap(), + vec![One; num_vertices], + ) + .unwrap(), k, ) } #[test] fn test_decisionminimumvertexcover_to_hamiltoniancircuit_structure_counts() { - let source = decision_mvc(3, &[(0, 1), (1, 2)], &[1, 1, 1], 1); + let source = decision_mvc(3, &[(0, 1), (1, 2)], 1); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -35,7 +35,7 @@ fn test_decisionminimumvertexcover_to_hamiltoniancircuit_structure_counts() { #[test] fn test_decisionminimumvertexcover_to_hamiltoniancircuit_closed_loop() { - let source = decision_mvc(3, &[(0, 1), (1, 2)], &[1, 1, 1], 1); + let source = decision_mvc(3, &[(0, 1), (1, 2)], 1); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); @@ -57,7 +57,7 @@ fn test_decisionminimumvertexcover_to_hamiltoniancircuit_closed_loop() { #[test] fn test_decisionminimumvertexcover_to_hamiltoniancircuit_ignores_isolated_vertices() { - let source = decision_mvc(3, &[(0, 1)], &[1, 1, 1], 1); + let source = decision_mvc(3, &[(0, 1)], 1); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); @@ -79,7 +79,7 @@ fn test_decisionminimumvertexcover_to_hamiltoniancircuit_ignores_isolated_vertic #[test] fn test_decisionminimumvertexcover_to_hamiltoniancircuit_fixed_yes_when_k_covers_all_active_vertices( ) { - let source = decision_mvc(3, &[(0, 1), (1, 2)], &[1, 1, 1], 3); + let source = decision_mvc(3, &[(0, 1), (1, 2)], 3); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -97,7 +97,7 @@ fn test_decisionminimumvertexcover_to_hamiltoniancircuit_fixed_yes_when_k_covers #[test] fn test_decisionminimumvertexcover_to_hamiltoniancircuit_fixed_no_when_k_zero() { - let source = decision_mvc(2, &[(0, 1)], &[1, 1], 0); + let source = decision_mvc(2, &[(0, 1)], 0); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -107,11 +107,31 @@ fn test_decisionminimumvertexcover_to_hamiltoniancircuit_fixed_no_when_k_zero() } #[test] -fn test_decisionminimumvertexcover_to_hamiltoniancircuit_rejects_non_unit_weights() { - let source = decision_mvc(2, &[(0, 1)], &[2, 1], 1); - let error = ReduceTo::>::reduce_to(&source).unwrap_err(); - assert!(matches!( - error, - crate::rules::ReductionError::InvalidTarget { .. } - )); +fn hamiltonian_edge_registers_only_unit_weight_vertex_cover() { + let sources = inventory::iter:: + .into_iter() + .filter(|entry| { + entry.source_name == "DecisionMinimumVertexCover" + && entry.target_name == "HamiltonianCircuit" + }) + .map(|entry| (entry.source_variant_fn)()) + .collect::>(); + assert_eq!( + sources, + vec![Decision::>::variant()] + ); +} + +#[test] +fn unit_cover_bound_handles_negative_and_empty_graphs() { + for (bound, expected) in [(-1, false), (0, true), (i64::MAX, true)] { + let source = decision_mvc(2, &[], bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let witness = BruteForce::new().solve(reduction.target_problem()).unwrap(); + assert_eq!(witness.is_some(), expected); + if let Some(witness) = witness { + let cover = reduction.extract_solution(&witness).unwrap(); + assert!(source.evaluate(&cover).unwrap().0); + } + } } diff --git a/src/unit_tests/rules/directedhamiltonianpath_ilp.rs b/src/unit_tests/rules/directedhamiltonianpath_ilp.rs index ad9c9f1bc..953af3b19 100644 --- a/src/unit_tests/rules/directedhamiltonianpath_ilp.rs +++ b/src/unit_tests/rules/directedhamiltonianpath_ilp.rs @@ -8,7 +8,7 @@ use crate::types::Or; #[test] fn test_reduction_creates_valid_ilp() { // Directed path: 0->1->2 (n=3) - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = DirectedHamiltonianPath::new(graph); let reduction: ReductionDirectedHamiltonianPathToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -21,7 +21,7 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_directedhamiltonianpath_to_ilp_closed_loop() { // Directed path: 0->1->2->3 - let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = DirectedHamiltonianPath::new(graph); // BruteForce to verify feasibility @@ -64,7 +64,8 @@ fn test_directedhamiltonianpath_to_ilp_issue_example() { (4, 5), (5, 1), ], - ); + ) + .unwrap(); let problem = DirectedHamiltonianPath::new(graph); let reduction: ReductionDirectedHamiltonianPathToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -83,21 +84,22 @@ fn test_directedhamiltonianpath_to_ilp_issue_example() { #[test] fn test_directedhamiltonianpath_to_ilp_no_path() { // No Hamiltonian path: 0->1, 0->2, but no outgoing arcs from 1 or 2 - let graph = DirectedGraph::new(3, vec![(0, 1), (0, 2)]); + let graph = DirectedGraph::new(3, vec![(0, 1), (0, 2)]).unwrap(); let problem = DirectedHamiltonianPath::new(graph); let reduction: ReductionDirectedHamiltonianPathToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(reduction.target_problem()); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Graph with no Hamiltonian path should be infeasible" ); } #[test] fn test_directedhamiltonianpath_to_ilp_bf_vs_ilp() { - let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = DirectedHamiltonianPath::new(graph); let reduction: ReductionDirectedHamiltonianPathToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); diff --git a/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs b/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs index d438da773..c2050b0bc 100644 --- a/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs @@ -20,7 +20,8 @@ fn feasible_instance() -> DirectedTwoCommodityIntegralFlow { (3, 4), (3, 5), ], - ), + ) + .unwrap(), vec![1; 8], 0, 4, @@ -29,13 +30,14 @@ fn feasible_instance() -> DirectedTwoCommodityIntegralFlow { 1, 1, ) + .unwrap() } fn infeasible_instance() -> DirectedTwoCommodityIntegralFlow { // Two commodities competing on a single arc with cap=1 // s1=0→t1=2 and s2=0→t2=2 both need to route 1 unit through the single arc (0,2) DirectedTwoCommodityIntegralFlow::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1, 1], 0, 2, @@ -44,6 +46,7 @@ fn infeasible_instance() -> DirectedTwoCommodityIntegralFlow { 1, 1, ) + .unwrap() } #[test] @@ -97,21 +100,24 @@ fn test_directedtwocommodityintegralflow_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionD2CIFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible flow instance should produce infeasible ILP" ); } #[test] fn test_directedtwocommodityintegralflow_to_ilp_disallows_using_other_commodity_source() { - let graph = DirectedGraph::new(4, vec![(2, 3), (3, 1)]); - let problem = DirectedTwoCommodityIntegralFlow::new(graph, vec![1, 1], 0, 1, 2, 3, 1, 0); + let graph = DirectedGraph::new(4, vec![(2, 3), (3, 1)]).unwrap(); + let problem = + DirectedTwoCommodityIntegralFlow::new(graph, vec![1, 1], 0, 1, 2, 3, 1, 0).unwrap(); let reduction: ReductionD2CIFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "commodity 1 must conserve flow at commodity 2's source in the ILP reduction" ); } @@ -150,7 +156,7 @@ fn test_directedtwocommodityintegralflow_to_ilp_bf_vs_ilp() { fn test_directedtwocommodityintegralflow_to_ilp_preserves_large_exact_capacity() { let capacity = crate::types::MAX_EXACT_F64_INTEGER + 1; let problem = DirectedTwoCommodityIntegralFlow::new( - DirectedGraph::new(2, vec![(0, 1)]), + DirectedGraph::new(2, vec![(0, 1)]).unwrap(), vec![capacity], 0, 1, @@ -158,7 +164,8 @@ fn test_directedtwocommodityintegralflow_to_ilp_preserves_large_exact_capacity() 1, 1, 1, - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let capacity_constraint = &reduction.target_problem().constraints()[0]; diff --git a/src/unit_tests/rules/disjointconnectingpaths_ilp.rs b/src/unit_tests/rules/disjointconnectingpaths_ilp.rs index 740f58586..dbc664a22 100644 --- a/src/unit_tests/rules/disjointconnectingpaths_ilp.rs +++ b/src/unit_tests/rules/disjointconnectingpaths_ilp.rs @@ -12,9 +12,10 @@ fn test_disjointconnectingpaths_to_ilp_closed_loop() { // Path (0,2): 0 - 1 - 2 (interior vertex 1, not a terminal) // Path (3,5): 3 - 4 - 5 (interior vertex 4, not a terminal) let source = DisjointConnectingPaths::new( - SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]), + SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]).unwrap(), vec![(0, 2), (3, 5)], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); assert_bf_vs_ilp(&source, &reduction); } @@ -22,9 +23,10 @@ fn test_disjointconnectingpaths_to_ilp_closed_loop() { #[test] fn test_disjointconnectingpaths_to_ilp_bf_vs_ilp() { let source = DisjointConnectingPaths::new( - SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]), + SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]).unwrap(), vec![(0, 2), (3, 5)], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } @@ -32,9 +34,10 @@ fn test_disjointconnectingpaths_to_ilp_bf_vs_ilp() { #[test] fn test_disjointconnectingpaths_to_ilp_forbids_using_another_pairs_terminal() { let source = DisjointConnectingPaths::new( - SimpleGraph::new(5, vec![(0, 1), (0, 2), (0, 3), (2, 4), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (0, 2), (0, 3), (2, 4), (3, 4)]).unwrap(), vec![(0, 1), (2, 3)], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let mut colliding_flow = vec![0; 20]; for index in [0, 13, 14] { @@ -52,9 +55,10 @@ fn test_disjointconnectingpaths_to_ilp_forbids_using_another_pairs_terminal() { #[test] fn test_disjointconnectingpaths_to_ilp_discards_disconnected_circulation() { let source = DisjointConnectingPaths::new( - SimpleGraph::new(7, vec![(0, 1), (2, 3), (4, 5), (4, 6), (5, 6)]), + SimpleGraph::new(7, vec![(0, 1), (2, 3), (4, 5), (4, 6), (5, 6)]).unwrap(), vec![(0, 1), (2, 3)], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let mut target_solution = vec![0; 20]; for index in [0, 4, 7, 8, 12] { diff --git a/src/unit_tests/rules/ensemblecomputation_ilp.rs b/src/unit_tests/rules/ensemblecomputation_ilp.rs index 3be3c0aaa..319cdca2b 100644 --- a/src/unit_tests/rules/ensemblecomputation_ilp.rs +++ b/src/unit_tests/rules/ensemblecomputation_ilp.rs @@ -30,14 +30,20 @@ fn test_ensemblecomputation_to_ilp_closed_loop() { fn test_ensemblecomputation_to_ilp_infeasible_budget() { let source = EnsembleComputation::new(3, vec![vec![0, 1, 2]], 1); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] fn test_ensemblecomputation_to_ilp_rejects_singleton_target() { let source = EnsembleComputation::new(3, vec![vec![0]], 2); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/eulerianpath_ilp.rs b/src/unit_tests/rules/eulerianpath_ilp.rs index 7d9f5c3c4..e09d64c67 100644 --- a/src/unit_tests/rules/eulerianpath_ilp.rs +++ b/src/unit_tests/rules/eulerianpath_ilp.rs @@ -9,7 +9,7 @@ use crate::types::Or; /// Canonical issue #1025 instance: V = {0,1,2}, A = [(0,1),(0,1),(1,2),(2,0)]. /// A witness exists: ordering (a_0, a_2, a_3, a_1) traces 0->1->2->0->1. fn issue_instance() -> EulerianPath { - EulerianPath::new(DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)])) + EulerianPath::new(DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)]).unwrap()) } #[test] @@ -82,13 +82,14 @@ fn test_eulerianpath_to_ilp_infeasible_no_instance() { // Two arcs sharing the same tail but disconnected heads. This breaks the // degree-balance criterion: vertex 0 has out-degree 2 / in-degree 0, so // no Eulerian trail exists. - let source = EulerianPath::new(DirectedGraph::new(3, vec![(0, 1), (0, 2)])); + let source = EulerianPath::new(DirectedGraph::new(3, vec![(0, 1), (0, 2)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); // The ILP must report infeasibility for a NO instance. let solution = ILPSolver::new().solve(reduction.target_problem()); - assert!( - solution.is_err(), + assert_eq!( + solution, + Err(crate::solvers::ILPSolveError::Infeasible), "ILP must be infeasible for a degree-unbalanced NO instance, got {:?}", solution ); @@ -98,7 +99,7 @@ fn test_eulerianpath_to_ilp_infeasible_no_instance() { fn test_eulerianpath_to_ilp_closed_circuit_with_loop() { // Loop + closed trail: arcs (0,0), (0,1), (1,0). // Trail (0,0) -> (0,1) -> (1,0) is a valid closed Eulerian trail. - let source = EulerianPath::new(DirectedGraph::new(2, vec![(0, 0), (0, 1), (1, 0)])); + let source = EulerianPath::new(DirectedGraph::new(2, vec![(0, 0), (0, 1), (1, 0)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() diff --git a/src/unit_tests/rules/exactcoverby3sets_algebraicequationsovergf2.rs b/src/unit_tests/rules/exactcoverby3sets_algebraicequationsovergf2.rs index 5d1fc643f..63bdf5b81 100644 --- a/src/unit_tests/rules/exactcoverby3sets_algebraicequationsovergf2.rs +++ b/src/unit_tests/rules/exactcoverby3sets_algebraicequationsovergf2.rs @@ -5,7 +5,7 @@ use crate::rules::{ReduceTo, ReductionResult}; #[test] fn test_exactcoverby3sets_to_algebraicequationsovergf2_closed_loop() { - let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]); + let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]).unwrap(); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); @@ -18,7 +18,7 @@ fn test_exactcoverby3sets_to_algebraicequationsovergf2_closed_loop() { #[test] fn test_exactcoverby3sets_to_algebraicequationsovergf2_structure() { - let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]); + let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]).unwrap(); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -43,14 +43,14 @@ fn test_exactcoverby3sets_to_algebraicequationsovergf2_structure() { #[test] fn test_exactcoverby3sets_to_algebraicequationsovergf2_extract_solution_is_identity() { - let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]); + let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]).unwrap(); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); assert_eq!( reduction - .extract_solution(&vec![true, false, true]) + .extract_solution(&vec![true, true, false]) .unwrap(), - vec![true, false, true] + vec![true, true, false] ); } diff --git a/src/unit_tests/rules/exactcoverby3sets_boundeddiameterspanningtree.rs b/src/unit_tests/rules/exactcoverby3sets_boundeddiameterspanningtree.rs index ba458bb61..e1c581f3c 100644 --- a/src/unit_tests/rules/exactcoverby3sets_boundeddiameterspanningtree.rs +++ b/src/unit_tests/rules/exactcoverby3sets_boundeddiameterspanningtree.rs @@ -6,13 +6,13 @@ use crate::topology::Graph; /// q = 2, m = 2: X = {0..5} with C = [{0,1,2}, {3,4,5}]. /// Both subsets together form the unique exact cover. fn yes_instance_simple() -> ExactCoverBy3Sets { - ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]) + ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]).unwrap() } /// q = 2, m = 2 but the two subsets overlap on element 0, /// so no exact cover exists. fn no_instance_simple() -> ExactCoverBy3Sets { - ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [0, 3, 4]]) + ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [0, 3, 4]]).unwrap() } #[test] @@ -82,11 +82,15 @@ fn test_exactcoverby3sets_to_boundeddiameterspanningtree_extract_solution() { let mut invalid = vec![false; target_config.len()]; invalid[2] = true; invalid[3] = true; - assert!(reduction.extract_solution(&invalid).is_err()); - assert!(reduction.extract_solution(&vec![]).is_err()); - assert!(reduction - .extract_solution(&vec![true; target_config.len()]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &invalid), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true; target_config.len()]), Ok(value) if { value.is_valid() }) + ); } #[test] @@ -103,13 +107,15 @@ fn test_exactcoverby3sets_to_boundeddiameterspanningtree_no_instance() { // exist here). Equivalently, the brute-force aggregate evaluates to // Or(false). assert!(BruteForce::new().solve(target).unwrap().is_none()); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); } #[test] fn test_exactcoverby3sets_to_boundeddiameterspanningtree_universe_boundaries() { for universe in [3, usize::MAX - usize::MAX % 3] { - let source = ExactCoverBy3Sets::new(universe, vec![]); + let source = ExactCoverBy3Sets::new(universe, vec![]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); assert_eq!(reduction.target_problem().num_vertices(), 2); @@ -118,9 +124,11 @@ fn test_exactcoverby3sets_to_boundeddiameterspanningtree_universe_boundaries() { .solve(reduction.target_problem()) .unwrap() .is_none()); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); } - let source = ExactCoverBy3Sets::new(0, vec![]); + let source = ExactCoverBy3Sets::new(0, vec![]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let witness = BruteForce::new() @@ -136,7 +144,7 @@ fn test_exactcoverby3sets_to_boundeddiameterspanningtree_universe_boundaries() { #[test] fn test_exactcoverby3sets_to_boundeddiameterspanningtree_duplicate_sets() { - let source = ExactCoverBy3Sets::new(3, vec![[0, 1, 2], [0, 1, 2]]); + let source = ExactCoverBy3Sets::new(3, vec![[0, 1, 2], [0, 1, 2]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let witnesses = BruteForce::new() diff --git a/src/unit_tests/rules/exactcoverby3sets_ilp.rs b/src/unit_tests/rules/exactcoverby3sets_ilp.rs index 06de42089..7a017eb0d 100644 --- a/src/unit_tests/rules/exactcoverby3sets_ilp.rs +++ b/src/unit_tests/rules/exactcoverby3sets_ilp.rs @@ -6,7 +6,7 @@ use crate::types::Or; #[test] fn test_reduction_creates_valid_ilp() { // Universe {0..5}, 3 triples - let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]); + let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]).unwrap(); let reduction: ReductionX3CToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -17,7 +17,8 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_exactcoverby3sets_to_ilp_bf_vs_ilp() { - let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4], [1, 2, 5]]); + let problem = + ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4], [1, 2, 5]]).unwrap(); let reduction: ReductionX3CToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -35,7 +36,7 @@ fn test_exactcoverby3sets_to_ilp_bf_vs_ilp() { #[test] fn test_solution_extraction() { - let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]); + let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]).unwrap(); let reduction: ReductionX3CToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = vec![1, 1]; // select both triples @@ -46,7 +47,7 @@ fn test_solution_extraction() { #[test] fn test_exactcoverby3sets_to_ilp_trivial() { - let problem = ExactCoverBy3Sets::new(0, vec![]); + let problem = ExactCoverBy3Sets::new(0, vec![]).unwrap(); let reduction: ReductionX3CToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/exactcoverby3sets_maximumsetpacking.rs b/src/unit_tests/rules/exactcoverby3sets_maximumsetpacking.rs index 146036ae1..ff3be75fa 100644 --- a/src/unit_tests/rules/exactcoverby3sets_maximumsetpacking.rs +++ b/src/unit_tests/rules/exactcoverby3sets_maximumsetpacking.rs @@ -10,7 +10,8 @@ fn test_exactcoverby3sets_to_maximumsetpacking_closed_loop() { let source = ExactCoverBy3Sets::new( 6, vec![[0, 1, 2], [0, 1, 3], [3, 4, 5], [2, 4, 5], [1, 3, 5]], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); @@ -26,7 +27,8 @@ fn test_exactcoverby3sets_to_maximumsetpacking_structure() { let source = ExactCoverBy3Sets::new( 6, vec![[0, 1, 2], [0, 1, 3], [3, 4, 5], [2, 4, 5], [1, 3, 5]], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -52,7 +54,7 @@ fn test_exactcoverby3sets_to_maximumsetpacking_structure() { fn test_exactcoverby3sets_to_maximumsetpacking_unsatisfiable() { // Universe {0,1,2,3,4,5} but subsets cannot form an exact cover: // all subsets share element 0 - let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [0, 3, 4], [0, 4, 5]]); + let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [0, 3, 4], [0, 4, 5]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -72,7 +74,7 @@ fn test_exactcoverby3sets_to_maximumsetpacking_unsatisfiable() { #[test] fn test_exactcoverby3sets_to_maximumsetpacking_optimal_value() { // Satisfiable instance: S0={0,1,2}, S1={3,4,5} form an exact cover - let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]); + let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/unit_tests/rules/exactcoverby3sets_minimumaxiomset.rs b/src/unit_tests/rules/exactcoverby3sets_minimumaxiomset.rs index 757f57c49..fc1bc604c 100644 --- a/src/unit_tests/rules/exactcoverby3sets_minimumaxiomset.rs +++ b/src/unit_tests/rules/exactcoverby3sets_minimumaxiomset.rs @@ -9,10 +9,11 @@ fn issue_yes_instance() -> ExactCoverBy3Sets { 6, vec![[0, 1, 2], [0, 3, 4], [2, 4, 5], [1, 3, 5], [0, 2, 4]], ) + .unwrap() } fn shared_zero_instance() -> ExactCoverBy3Sets { - ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [0, 3, 4], [0, 4, 5]]) + ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [0, 3, 4], [0, 4, 5]]).unwrap() } #[test] diff --git a/src/unit_tests/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs b/src/unit_tests/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs index aeb36f7a0..42da4200e 100644 --- a/src/unit_tests/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs +++ b/src/unit_tests/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs @@ -7,11 +7,11 @@ use crate::traits::Problem; use crate::types::Min; fn issue_yes_instance() -> ExactCoverBy3Sets { - ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]) + ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]).unwrap() } fn no_cover_instance() -> ExactCoverBy3Sets { - ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [0, 3, 4], [0, 4, 5]]) + ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [0, 3, 4], [0, 4, 5]]).unwrap() } #[test] diff --git a/src/unit_tests/rules/exactcoverby3sets_staffscheduling.rs b/src/unit_tests/rules/exactcoverby3sets_staffscheduling.rs index ea050fe01..67bfdbeea 100644 --- a/src/unit_tests/rules/exactcoverby3sets_staffscheduling.rs +++ b/src/unit_tests/rules/exactcoverby3sets_staffscheduling.rs @@ -10,7 +10,8 @@ use crate::traits::Problem; fn test_exactcoverby3sets_to_staffscheduling_closed_loop() { // Universe {0,1,2,3,4,5}, subsets [{0,1,2}, {3,4,5}, {0,3,4}, {1,2,5}] // Exact cover: S0={0,1,2} + S1={3,4,5} - let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4], [1, 2, 5]]); + let source = + ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4], [1, 2, 5]]).unwrap(); let result = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = result.target_problem(); @@ -31,7 +32,7 @@ fn test_exactcoverby3sets_to_staffscheduling_closed_loop() { fn test_exactcoverby3sets_to_staffscheduling_no_solution() { // Universe {0,1,2,3,4,5} with overlapping subsets that cannot form exact cover // All subsets share element 0 - let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [0, 3, 4], [0, 4, 5]]); + let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [0, 3, 4], [0, 4, 5]]).unwrap(); let result = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(result.target_problem()).unwrap(); @@ -45,7 +46,7 @@ fn test_exactcoverby3sets_to_staffscheduling_no_solution() { fn test_exactcoverby3sets_to_staffscheduling_unique_cover() { // Universe {0,1,2,3,4,5,6,7,8} (q=3) // Only one exact cover: S0 + S1 + S2 - let source = ExactCoverBy3Sets::new(9, vec![[0, 1, 2], [3, 4, 5], [6, 7, 8]]); + let source = ExactCoverBy3Sets::new(9, vec![[0, 1, 2], [3, 4, 5], [6, 7, 8]]).unwrap(); let result = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = result.target_problem(); @@ -78,7 +79,8 @@ fn test_exactcoverby3sets_to_staffscheduling_unique_cover() { #[test] fn test_exactcoverby3sets_to_staffscheduling_extract_solution() { // Verify extract_solution maps correctly - let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4], [1, 2, 5]]); + let source = + ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4], [1, 2, 5]]).unwrap(); let result = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); // StaffScheduling config: [1, 1, 0, 0] means 1 worker on schedule 0 and 1 on schedule 1 @@ -89,16 +91,16 @@ fn test_exactcoverby3sets_to_staffscheduling_extract_solution() { // Verify the extracted solution is valid in the source assert!(source.evaluate(&extracted).unwrap().0); - // Config with 0 workers everywhere should extract to all-zero (no subsets selected) - let empty_config = vec![0, 0, 0, 0]; - let extracted_empty = result.extract_solution(&empty_config).unwrap(); - assert_eq!(extracted_empty, vec![false, false, false, false]); + // No workers cannot cover the required shifts. + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&result), &vec![0, 0, 0, 0]), Ok(value) if { value.is_valid() }) + ); } #[test] fn test_exactcoverby3sets_to_staffscheduling_schedule_structure() { // Verify the schedule patterns are correctly constructed - let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]); + let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5]]).unwrap(); let result = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = result.target_problem(); diff --git a/src/unit_tests/rules/exactcoverby3sets_subsetproduct.rs b/src/unit_tests/rules/exactcoverby3sets_subsetproduct.rs index dac72241d..4bf288bdc 100644 --- a/src/unit_tests/rules/exactcoverby3sets_subsetproduct.rs +++ b/src/unit_tests/rules/exactcoverby3sets_subsetproduct.rs @@ -6,7 +6,7 @@ use num_bigint::BigUint; #[test] fn test_exactcoverby3sets_to_subsetproduct_closed_loop() { - let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]); + let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); @@ -19,7 +19,7 @@ fn test_exactcoverby3sets_to_subsetproduct_closed_loop() { #[test] fn test_exactcoverby3sets_to_subsetproduct_structure() { - let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]); + let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -35,21 +35,21 @@ fn test_exactcoverby3sets_to_subsetproduct_structure() { #[test] fn test_exactcoverby3sets_to_subsetproduct_extract_solution_is_identity() { - let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]); + let source = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4]]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); assert_eq!( reduction - .extract_solution(&vec![true, false, true]) + .extract_solution(&vec![true, true, false]) .unwrap(), - vec![true, false, true] + vec![true, true, false] ); } #[test] fn test_exactcoverby3sets_to_subsetproduct_supports_large_universe() { - let source = ExactCoverBy3Sets::new(18, vec![[0, 1, 2], [15, 16, 17]]); + let source = ExactCoverBy3Sets::new(18, vec![[0, 1, 2], [15, 16, 17]]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/unit_tests/rules/expectedretrievalcost_ilp.rs b/src/unit_tests/rules/expectedretrievalcost_ilp.rs index ae9b2af03..86a9b561a 100644 --- a/src/unit_tests/rules/expectedretrievalcost_ilp.rs +++ b/src/unit_tests/rules/expectedretrievalcost_ilp.rs @@ -65,20 +65,25 @@ fn test_solution_extraction() { let reduction: ReductionERCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - // record 0 -> sector 0, record 1 -> sector 1 - // x_{0,0}=1, x_{0,1}=0, x_{1,0}=0, x_{1,1}=1 - let mut ilp_solution = vec![0_i64; 4 + 16]; // n + n^2 - // x vars - ilp_solution[0] = 1; // x_{0,0} - ilp_solution[3] = 1; // x_{1,1} - // z vars: z_{r,s,r',s'} at offset 4 + (r*2+s)*4 + (r'*2+s') - // z_{0,0,0,0} = x_{0,0}*x_{0,0} = 1: offset 4 + 0*4 + 0 = 4 - ilp_solution[4] = 1; - // z_{1,1,1,1} = x_{1,1}*x_{1,1} = 1: offset 4 + 3*4 + 3 = 4+15=19 - ilp_solution[19] = 1; - - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); - assert_eq!(extracted, vec![0, 1]); + for assignment in [vec![0, 0], vec![0, 1], vec![1, 0], vec![1, 1]] { + let mut target = vec![0; reduction.target_problem().num_vars()]; + for (r, §or) in assignment.iter().enumerate() { + target[reduction.x_var(r, sector)] = 1; + } + for (r, §or) in assignment.iter().enumerate() { + for (other, &other_sector) in assignment.iter().enumerate() { + target[reduction.z_var(r, sector, other, other_sector)] = 1; + } + } + assert_eq!(reduction.extract_solution(&target).unwrap(), assignment); + assert_eq!( + reduction + .target_problem() + .evaluate_objective(&target) + .unwrap(), + problem.expected_cost(&assignment).unwrap().unwrap() + ); + } } #[test] diff --git a/src/unit_tests/rules/factoring_circuit.rs b/src/unit_tests/rules/factoring_circuit.rs index 5b16f2702..4dacf41cc 100644 --- a/src/unit_tests/rules/factoring_circuit.rs +++ b/src/unit_tests/rules/factoring_circuit.rs @@ -1,10 +1,10 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; use crate::traits::Problem; use num_bigint::BigUint; use std::collections::HashMap; -include!("../jl_helpers.rs"); #[test] fn test_read_bit() { @@ -396,10 +396,12 @@ fn test_factoring_to_circuit_zero_width_closed_loop() { fn test_factoring_to_circuit_rejects_invalid_certificates() { let source = Factoring::with_factor_bits(6, 2, 2); let reduction = ReduceTo::::reduce_to(&source).unwrap(); - assert!(reduction.extract_solution(&vec![]).is_err()); - assert!(reduction - .extract_solution(&vec![false; reduction.target_problem().num_variables()]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; reduction.target_problem().num_variables()]), Ok(value) if { value.is_valid() }) + ); let values = evaluate_multiplier_circuit(&reduction, 1, 1); let config = reduction .target_problem() @@ -407,7 +409,9 @@ fn test_factoring_to_circuit_rejects_invalid_certificates() { .iter() .map(|name| values[name]) .collect(); - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } #[test] diff --git a/src/unit_tests/rules/factoring_ilp.rs b/src/unit_tests/rules/factoring_ilp.rs index 472e97016..8d5c4b2d0 100644 --- a/src/unit_tests/rules/factoring_ilp.rs +++ b/src/unit_tests/rules/factoring_ilp.rs @@ -171,7 +171,11 @@ fn test_infeasible_target_too_large() { let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(ilp); - assert!(result.is_err(), "Should be infeasible"); + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), + "Should be infeasible" + ); } #[test] @@ -215,7 +219,8 @@ fn test_solution_extraction() { // z_00 = p_0 * q_0 = 0, z_01 = p_0 * q_1 = 0 // z_10 = p_1 * q_0 = 1, z_11 = p_1 * q_1 = 1 // Variables: [p0, p1, q0, q1, z00, z01, z10, z11, c0, c1, c2, c3] - let ilp_solution = vec![0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0]; + // Each product column already matches 0110, so every carry is zero. + let ilp_solution = vec![0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0]; let extracted = reduction.extract_solution(&ilp_solution).unwrap(); assert_eq!(extracted, (BigUint::from(2u32), BigUint::from(3u32))); @@ -277,7 +282,10 @@ fn test_oversized_biguint_target_makes_ilp_infeasible() { let target = BigUint::from(1u32) << 70; let problem = Factoring::with_factor_bits(target, 2, 2); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/feasibleregisterassignment_ilp.rs b/src/unit_tests/rules/feasibleregisterassignment_ilp.rs index 6b18e1e2a..01943655c 100644 --- a/src/unit_tests/rules/feasibleregisterassignment_ilp.rs +++ b/src/unit_tests/rules/feasibleregisterassignment_ilp.rs @@ -4,7 +4,7 @@ use crate::traits::Problem; use crate::types::Or; fn feasible_example() -> FeasibleRegisterAssignment { - FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]) + FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]).unwrap() } #[test] @@ -37,11 +37,13 @@ fn test_feasible_register_assignment_to_ilp_closed_loop() { #[test] fn test_feasible_register_assignment_to_ilp_infeasible() { - let source = FeasibleRegisterAssignment::new(3, vec![(0, 1), (0, 2), (1, 2)], 1, vec![0, 0, 0]); + let source = + FeasibleRegisterAssignment::new(3, vec![(0, 1), (0, 2), (1, 2)], 1, vec![0, 0, 0]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "register-conflict source instance should reduce to an infeasible ILP" ); } diff --git a/src/unit_tests/rules/flowshopscheduling_ilp.rs b/src/unit_tests/rules/flowshopscheduling_ilp.rs index 34da74e87..c126548d1 100644 --- a/src/unit_tests/rules/flowshopscheduling_ilp.rs +++ b/src/unit_tests/rules/flowshopscheduling_ilp.rs @@ -7,7 +7,7 @@ use crate::types::Or; #[test] fn test_flowshopscheduling_to_ilp_closed_loop() { // 2 machines, 3 jobs, deadline 10 - let problem = FlowShopScheduling::new(2, vec![vec![2, 3], vec![3, 2], vec![1, 4]], 10); + let problem = FlowShopScheduling::new(2, vec![vec![2, 3], vec![3, 2], vec![1, 4]], 10).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf = BruteForce::new(); @@ -31,10 +31,11 @@ fn test_flowshopscheduling_to_ilp_closed_loop() { #[test] fn test_flowshopscheduling_to_ilp_infeasible() { // 2 machines, 3 jobs with large processing times, very tight deadline - let problem = FlowShopScheduling::new(2, vec![vec![5, 5], vec![5, 5], vec![5, 5]], 6); + let problem = FlowShopScheduling::new(2, vec![vec![5, 5], vec![5, 5], vec![5, 5]], 6).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible FSS should produce infeasible ILP" ); } @@ -42,7 +43,7 @@ fn test_flowshopscheduling_to_ilp_infeasible() { #[test] fn test_flowshopscheduling_to_ilp_single_job() { // 2 machines, 1 job, deadline 10 - let problem = FlowShopScheduling::new(2, vec![vec![3, 4]], 10); + let problem = FlowShopScheduling::new(2, vec![vec![3, 4]], 10).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) @@ -53,7 +54,7 @@ fn test_flowshopscheduling_to_ilp_single_job() { #[test] fn test_flowshopscheduling_to_ilp_bf_vs_ilp() { - let problem = FlowShopScheduling::new(2, vec![vec![2, 3], vec![3, 2], vec![1, 4]], 10); + let problem = FlowShopScheduling::new(2, vec![vec![2, 3], vec![3, 2], vec![1, 4]], 10).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf = BruteForce::new(); diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index df044287b..e74524409 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -12,7 +12,6 @@ use crate::registry::ProblemCategory; use crate::rules::graph::{ReductionMode, ReductionStep}; use crate::rules::registry::{ReductionEntry, ReductionParameterDeclarations}; use crate::rules::traits::{AggregateReductionResult, ReductionResult}; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::{One, ProblemParameters, Sum}; @@ -84,7 +83,12 @@ impl Problem for AggregateChainSource { type Solution = Vec; type Value = Sum; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", 1usize as u64)]) + } fn evaluate( &self, @@ -99,8 +103,12 @@ impl Problem for AggregateChainSource { } impl crate::solvers::BruteForceProblem for AggregateChainSource { - fn dimensions(&self) -> Vec { - vec![1] + fn num_variables(&self) -> Result { + Ok(1usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([1][variable]) } } @@ -109,7 +117,12 @@ impl Problem for AggregateChainMiddle { type Solution = Vec; type Value = Sum; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", 1usize as u64)]) + } fn evaluate( &self, @@ -124,8 +137,12 @@ impl Problem for AggregateChainMiddle { } impl crate::solvers::BruteForceProblem for AggregateChainMiddle { - fn dimensions(&self) -> Vec { - vec![1] + fn num_variables(&self) -> Result { + Ok(1usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([1][variable]) } } @@ -134,7 +151,12 @@ impl Problem for AggregateChainTarget { type Solution = Vec; type Value = Sum; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", 1usize as u64)]) + } fn evaluate( &self, @@ -149,23 +171,32 @@ impl Problem for AggregateChainTarget { } impl crate::solvers::BruteForceProblem for AggregateChainTarget { - fn dimensions(&self) -> Vec { - vec![1] + fn num_variables(&self) -> Result { + Ok(1usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([1][variable]) } } impl Problem for NaturalVariantProblem { const NAME: &'static str = "NaturalVariantProblem"; type Solution = Vec; - type Value = Sum; + type Value = crate::types::Max; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", 1usize as u64)]) + } fn evaluate( &self, config: &Self::Solution, ) -> Result { - Ok(Sum(config.iter().sum::() as u64)) + Ok(crate::types::Max(Some(config.iter().sum::() as u64))) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -174,8 +205,12 @@ impl Problem for NaturalVariantProblem { } impl crate::solvers::BruteForceProblem for NaturalVariantProblem { - fn dimensions(&self) -> Vec { - vec![1] + fn num_variables(&self) -> Result { + Ok(1usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([1][variable]) } } @@ -267,7 +302,7 @@ impl ReductionResult for SourceToMiddleWitnessResult { fn reduce_source_to_middle_witness( any: &dyn Any, -) -> Result, crate::rules::ReductionError> { +) -> Result { any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { source_problem: AggregateChainSource::NAME, @@ -275,14 +310,18 @@ fn reduce_source_to_middle_witness( expected: std::any::type_name::(), }, )?; - Ok(Box::new(SourceToMiddleWitnessResult { - target: AggregateChainMiddle, - })) + Ok(crate::rules::registry::ExecutedStep { + witness: std::rc::Rc::new(SourceToMiddleWitnessResult { + target: AggregateChainMiddle, + }), + aggregate: None, + interpret_optimum: None, + }) } fn fail_source_to_middle_witness( _any: &dyn Any, -) -> Result, crate::rules::ReductionError> { +) -> Result { Err(crate::rules::ReductionError::InvalidTarget { source_problem: AggregateChainSource::NAME, target_problem: AggregateChainMiddle::NAME, @@ -294,7 +333,7 @@ static SHARED_PREFIX_EXECUTIONS: AtomicUsize = AtomicUsize::new(0); fn reduce_counted_source_to_middle_witness( any: &dyn Any, -) -> Result, crate::rules::ReductionError> { +) -> Result { SHARED_PREFIX_EXECUTIONS.fetch_add(1, Ordering::SeqCst); reduce_source_to_middle_witness(any) } @@ -321,7 +360,7 @@ impl ReductionResult for MiddleToTargetWitnessResult { fn reduce_middle_to_target_witness( any: &dyn Any, -) -> Result, crate::rules::ReductionError> { +) -> Result { any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { source_problem: AggregateChainMiddle::NAME, @@ -329,14 +368,18 @@ fn reduce_middle_to_target_witness( expected: std::any::type_name::(), }, )?; - Ok(Box::new(MiddleToTargetWitnessResult { - target: AggregateChainTarget, - })) + Ok(crate::rules::registry::ExecutedStep { + witness: std::rc::Rc::new(MiddleToTargetWitnessResult { + target: AggregateChainTarget, + }), + aggregate: None, + interpret_optimum: None, + }) } fn reduce_natural_variant_witness( any: &dyn Any, -) -> Result, crate::rules::ReductionError> { +) -> Result { let source = any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { source_problem: NaturalVariantProblem::NAME, @@ -344,10 +387,14 @@ fn reduce_natural_variant_witness( expected: std::any::type_name::(), }, )?; - Ok(Box::new(crate::rules::VariantReductionResult::< - NaturalVariantProblem, - NaturalVariantProblem, - >::new(source.clone()))) + Ok(crate::rules::registry::ExecutedStep { + witness: std::rc::Rc::new(crate::rules::VariantReductionResult::< + NaturalVariantProblem, + NaturalVariantProblem, + >::new(source.clone())), + aggregate: None, + interpret_optimum: None, + }) } fn build_two_node_graph( @@ -418,7 +465,7 @@ fn execute_paths_executes_a_shared_prefix_once() { ), ], ); - let paths = vec![ + let mut paths = vec![ named_path(&[AggregateChainSource::NAME, AggregateChainMiddle::NAME]), named_path(&[ AggregateChainSource::NAME, @@ -427,11 +474,31 @@ fn execute_paths_executes_a_shared_prefix_once() { ]), ]; + paths.push(paths[0].clone()); + paths.push(paths[1].clone()); + let executed = graph .execute_paths(&paths, &AggregateChainSource) .expect("both paths are executable"); - assert_eq!(executed.len(), 2); + assert_eq!(executed.len(), 4); + for (path, execution) in paths.iter().zip(&executed) { + assert_eq!(execution.steps.len(), path.len()); + assert_eq!( + execution + .extract_solution::, _>(&vec![1usize]) + .unwrap(), + vec![1] + ); + } + assert!(std::rc::Rc::ptr_eq( + &executed[0].steps[0].witness, + &executed[3].steps[0].witness + )); + assert!(std::rc::Rc::ptr_eq( + &executed[1].steps[1].witness, + &executed[3].steps[1].witness + )); assert_eq!(SHARED_PREFIX_EXECUTIONS.load(Ordering::SeqCst), 1); } @@ -693,7 +760,8 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { .expect("expected aggregate reduction chain"); assert_eq!( - chain.target_problem::().dimensions(), + crate::solvers::cartesian_dimensions(chain.target_problem::()) + .unwrap(), vec![1] ); assert_eq!(chain.extract_value_dyn(json!(7)), json!(12)); @@ -1528,9 +1596,10 @@ fn test_reduce_along_path_direct() { .expect("direct route"); // Just verify the path can produce a chain with a dummy source let source = MaximumIndependentSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let chain = graph .reduce_along_path(&rpath, &source as &dyn std::any::Any) .expect("direct reduction should not fail"); @@ -1552,9 +1621,10 @@ fn test_reduction_chain_direct() { .expect("direct route"); let problem = MaximumIndependentSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let chain = graph .reduce_along_path(&rpath, &problem as &dyn std::any::Any) .unwrap() @@ -1583,9 +1653,10 @@ fn test_reduction_chain_multi_step() { .expect("direct route"); let problem = MaximumIndependentSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let chain = graph .reduce_along_path(&rpath, &problem as &dyn std::any::Any) .unwrap() @@ -1631,7 +1702,7 @@ fn test_reduction_chain_with_variant_reductions() { // Create a small UnitDiskGraph MIS problem (triangle of close nodes) let udg = UnitDiskGraph::new(vec![(0.0, 0.0), (0.5, 0.0), (0.25, 0.4)], 1.0).unwrap(); - let mis = MaximumIndependentSet::new(udg, vec![1i64, 1, 1]); + let mis = MaximumIndependentSet::new(udg, vec![1i64, 1, 1]).unwrap(); let chain = graph .reduce_along_path(&rpath, &mis as &dyn std::any::Any) @@ -1805,9 +1876,10 @@ fn test_variant_complexity() { #[test] fn test_compute_problem_parameters_uses_exact_variant_executor() { let problem = MaximumIndependentSet::::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1, 1, 1, 1], - ); + ) + .unwrap(); let variant = ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); let size = @@ -1916,3 +1988,119 @@ fn test_composed_path_parameters_transform_evaluation() { assert_eq!(final_size.get("num_vertices"), Some(10)); assert_eq!(final_size.get("num_edges"), Some(20)); } + +#[test] +fn witness_and_value_mapping_share_one_executed_construction() { + use crate::rules::registry::ExecutedStep; + use std::rc::Rc; + + static CONSTRUCTIONS: AtomicUsize = AtomicUsize::new(0); + + let chain = crate::rules::ReductionChain::execute( + &AggregateChainSource, + &[|_| { + CONSTRUCTIONS.fetch_add(1, Ordering::SeqCst); + let result = Rc::new(SourceToMiddleWitnessResult { + target: AggregateChainMiddle, + }); + Ok(ExecutedStep { + aggregate: Some(result.clone()), + interpret_optimum: None, + witness: result, + }) + }], + ) + .unwrap(); + let step = &chain.steps[0]; + let aggregate = step.aggregate.as_ref().unwrap(); + assert!(std::ptr::eq( + step.witness.target_problem_any(), + aggregate.target_problem_any(), + )); + let witness = vec![1usize]; + assert_eq!( + chain.extract_solution::, _>(&witness).unwrap(), + witness + ); + assert_eq!( + aggregate.extract_value_dyn(serde_json::json!(7)), + serde_json::json!(7) + ); + assert_eq!(CONSTRUCTIONS.load(Ordering::SeqCst), 1); +} + +impl AggregateReductionResult for SourceToMiddleWitnessResult { + type Source = AggregateChainSource; + type Target = AggregateChainMiddle; + fn target_problem(&self) -> &Self::Target { + &self.target + } + fn extract_value(&self, value: Sum) -> Sum { + value + } +} + +#[test] +fn composed_witness_agrees_across_direct_chain_path_and_json() { + use crate::rules::ReduceTo; + type Cover = MinimumVertexCover; + type IndependentSet = MaximumIndependentSet; + let source = Cover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1; 3], + ) + .unwrap(); + let first = ReduceTo::::reduce_to(&source).unwrap(); + let second = ReduceTo::>::reduce_to(first.target_problem()).unwrap(); + let third = ReduceTo::>::reduce_to(second.target_problem()).unwrap(); + let target_solution = vec![1i64, 0, 1]; + assert!(third + .target_problem() + .evaluate(&target_solution) + .unwrap() + .is_valid()); + let expected = first + .extract_solution( + &second + .extract_solution(&third.extract_solution(&target_solution).unwrap()) + .unwrap(), + ) + .unwrap(); + assert_eq!(expected, vec![false, true, false]); + let path = ReductionPath { + steps: [ + (Cover::NAME, Cover::variant()), + (IndependentSet::NAME, IndependentSet::variant()), + ( + MaximumSetPacking::::NAME, + MaximumSetPacking::::variant(), + ), + (ILP::::NAME, ILP::::variant()), + ] + .into_iter() + .map(|(name, variant)| ReductionStep { + name: name.into(), + variant: ReductionGraph::variant_to_map(&variant), + }) + .collect(), + }; + let graph = ReductionGraph::new(); + let chain = graph.reduce_along_path(&path, &source).unwrap().unwrap(); + let executed = graph.execute_paths(&[path], &source).unwrap(); + assert_eq!( + chain + .extract_solution::, _>(&target_solution) + .unwrap(), + expected + ); + assert_eq!( + executed[0] + .extract_solution::, _>(&target_solution) + .unwrap(), + expected + ); + assert_eq!( + chain.extract_solution_json(json!(target_solution)).unwrap(), + json!(expected) + ); +} diff --git a/src/unit_tests/rules/graphpartitioning_ilp.rs b/src/unit_tests/rules/graphpartitioning_ilp.rs index dc50e06a1..5a6f16d8c 100644 --- a/src/unit_tests/rules/graphpartitioning_ilp.rs +++ b/src/unit_tests/rules/graphpartitioning_ilp.rs @@ -20,7 +20,8 @@ fn canonical_instance() -> GraphPartitioning { (3, 5), (4, 5), ], - ); + ) + .unwrap(); GraphPartitioning::new(graph) } @@ -52,7 +53,7 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_reduction_constraint_shape() { - let problem = GraphPartitioning::new(SimpleGraph::new(2, vec![(0, 1)])); + let problem = GraphPartitioning::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap()); let reduction: ReductionGraphPartitioningToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -99,7 +100,7 @@ fn test_graphpartitioning_to_ilp_closed_loop() { #[test] fn test_odd_vertices_reduce_to_infeasible_ilp() { - let problem = GraphPartitioning::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = GraphPartitioning::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction: ReductionGraphPartitioningToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/graphpartitioning_qubo.rs b/src/unit_tests/rules/graphpartitioning_qubo.rs index b378a227c..f8f8f5488 100644 --- a/src/unit_tests/rules/graphpartitioning_qubo.rs +++ b/src/unit_tests/rules/graphpartitioning_qubo.rs @@ -4,20 +4,23 @@ use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization use crate::topology::SimpleGraph; fn example_problem() -> GraphPartitioning { - GraphPartitioning::new(SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (1, 2), - (1, 3), - (2, 3), - (2, 4), - (3, 4), - (3, 5), - (4, 5), - ], - )) + GraphPartitioning::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (2, 3), + (2, 4), + (3, 4), + (3, 5), + (4, 5), + ], + ) + .unwrap(), + ) } #[test] @@ -42,7 +45,7 @@ fn test_graphpartitioning_to_qubo_matrix_matches_issue_example() { let expected_diagonal = [-48, -47, -46, -46, -47, -48]; for (index, expected) in expected_diagonal.into_iter().enumerate() { - assert_eq!(qubo.get(index, index), Some(&expected)); + assert_eq!(qubo.get(index, index), Some(expected)); } let edge_pairs = [ @@ -57,12 +60,12 @@ fn test_graphpartitioning_to_qubo_matrix_matches_issue_example() { (4, 5), ]; for &(u, v) in &edge_pairs { - assert_eq!(qubo.get(u, v), Some(&18), "edge ({u}, {v})"); + assert_eq!(qubo.get(u, v), Some(18), "edge ({u}, {v})"); } let non_edge_pairs = [(0, 3), (0, 4), (0, 5), (1, 4), (1, 5), (2, 5)]; for &(u, v) in &non_edge_pairs { - assert_eq!(qubo.get(u, v), Some(&20), "non-edge ({u}, {v})"); + assert_eq!(qubo.get(u, v), Some(20), "non-edge ({u}, {v})"); } } @@ -77,6 +80,6 @@ fn test_graphpartitioning_to_qubo_canonical_example_spec() { assert_eq!(example.source.problem, "GraphPartitioning"); assert_eq!(example.target.problem, "QUBO"); - assert_eq!(example.target.instance["num_vars"], 6); + assert_eq!(example.target.instance["matrix"]["nrows"], 6); assert!(!example.solutions.is_empty()); } diff --git a/src/unit_tests/rules/hamiltoniancircuit_biconnectivityaugmentation.rs b/src/unit_tests/rules/hamiltoniancircuit_biconnectivityaugmentation.rs index 490356692..8b74fcb53 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_biconnectivityaugmentation.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_biconnectivityaugmentation.rs @@ -78,7 +78,8 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_extract_solution() { #[test] fn test_hamiltoniancircuit_to_biconnectivityaugmentation_no_circuit() { // Path graph 0-1-2-3: no Hamiltonian circuit (endpoints have degree 1) - let source = HamiltonianCircuit::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let source = + HamiltonianCircuit::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -134,7 +135,7 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_small_graphs() { } else { vec![] }; - let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges)); + let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges).unwrap()); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); @@ -143,7 +144,9 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_small_graphs() { assert_eq!(target.num_potential_edges(), 0); assert_eq!(*target.budget(), 0); assert!(!target.evaluate(&vec![]).unwrap().0); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); assert!(BruteForce::new().solve(&source).unwrap().is_none()); assert!(BruteForce::new().solve(target).unwrap().is_none()); } @@ -165,7 +168,7 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_all_graphs_and_certific // Native SimpleGraph inputs can contain loops and repeated edges. edges.extend(edges.clone()); edges.extend((0..n).map(|v| (v, v))); - let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges)); + let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges).unwrap()); let reduction = ReduceTo::>::reduce_to(&source) .unwrap(); @@ -173,9 +176,8 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_all_graphs_and_certific for mask in 0..1usize << pairs.len() { let config: Vec<_> = (0..pairs.len()).map(|i| mask & (1 << i) != 0).collect(); let feasible = reduction.target_problem().evaluate(&config).unwrap().0; - let extracted = reduction.extract_solution(&config); - assert_eq!(extracted.is_ok(), feasible); - if let Ok(circuit) = extracted { + if feasible { + let circuit = reduction.extract_solution(&config).unwrap(); assert!(source.evaluate(&circuit).unwrap().0); target_yes = true; } @@ -194,10 +196,18 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_rejects_infeasible_cert let reduction = ReduceTo::>::reduce_to(&source).unwrap(); // A spanning cycle made only of non-edges exceeds the budget and is not a source cycle. - assert!(reduction.extract_solution(&vec![true; 3]).is_err()); - assert!(reduction.extract_solution(&vec![false; 3]).is_err()); - assert!(reduction.extract_solution(&vec![true; 2]).is_err()); - assert!(reduction.extract_solution(&vec![true; 4]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true; 3]), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; 3]), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true; 2]), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true; 4]), Ok(value) if { value.is_valid() }) + ); let source = HamiltonianCircuit::new(SimpleGraph::complete(6)); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); @@ -208,5 +218,7 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_rejects_infeasible_cert .iter() .map(|&(u, v, _)| (u < 3) == (v < 3)) .collect(); - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } diff --git a/src/unit_tests/rules/hamiltoniancircuit_hamiltonianpath.rs b/src/unit_tests/rules/hamiltoniancircuit_hamiltonianpath.rs index 4f0482880..3a2c86632 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_hamiltonianpath.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_hamiltonianpath.rs @@ -89,7 +89,8 @@ fn test_hamiltoniancircuit_to_hamiltonianpath_extract_reversed() { #[test] fn test_hamiltoniancircuit_to_hamiltonianpath_no_circuit() { // Path graph 0-1-2-3: no Hamiltonian circuit (vertices 0 and 3 have degree 1) - let source = HamiltonianCircuit::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let source = + HamiltonianCircuit::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -119,7 +120,7 @@ fn test_hamiltoniancircuit_to_hamiltonianpath_triangle() { #[test] fn test_hamiltoniancircuit_to_hamiltonianpath_two_vertex_special_case_is_unsatisfiable() { - let source = HamiltonianCircuit::new(SimpleGraph::new(2, vec![(0, 1)])); + let source = HamiltonianCircuit::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/unit_tests/rules/hamiltoniancircuit_longestcircuit.rs b/src/unit_tests/rules/hamiltoniancircuit_longestcircuit.rs index e7d230743..774491b96 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_longestcircuit.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_longestcircuit.rs @@ -24,9 +24,12 @@ fn test_hamiltoniancircuit_aggregate_requires_a_spanning_cycle() { crate::types::Or(expected), ); } - let short_cycle = HamiltonianCircuit::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2)])); + let short_cycle = + HamiltonianCircuit::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&short_cycle).unwrap(); - assert!(reduction.extract_solution(&vec![true; 3]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true; 3]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } #[test] @@ -113,7 +116,7 @@ fn test_hamiltoniancircuit_extraction_matches_all_small_target_configurations() .enumerate() .filter_map(|(i, &edge)| ((graph_mask >> i) & 1 == 1).then_some(edge)) .collect(); - let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges)); + let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges).unwrap()); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = crate::rules::AggregateReductionResult::target_problem(&reduction); @@ -123,19 +126,14 @@ fn test_hamiltoniancircuit_extraction_matches_all_small_target_configurations() .collect(); let value = target.evaluate(&config).unwrap(); let certifies = value.0 == Some(n as i64); - let extracted = reduction.extract_solution(&config); - assert_eq!( - extracted.is_ok(), - certifies, - "n={n}, graph={graph_mask}, config={mask}" - ); - if let Ok(order) = extracted { + if certifies { + let order = reduction.extract_solution(&config).unwrap(); assert!(source.evaluate(&order).unwrap().0); } } - assert!(reduction - .extract_solution(&vec![false; target.num_edges() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_edges() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } diff --git a/src/unit_tests/rules/hamiltoniancircuit_quadraticassignment.rs b/src/unit_tests/rules/hamiltoniancircuit_quadraticassignment.rs index 466d811f9..9c856c870 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_quadraticassignment.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_quadraticassignment.rs @@ -128,7 +128,7 @@ fn test_prism_graph_hc_via_qap_ilp_roundtrip() { (1, 4), (2, 5), ]; - let hc = HamiltonianCircuit::new(SimpleGraph::new(6, edges)); + let hc = HamiltonianCircuit::new(SimpleGraph::new(6, edges).unwrap()); // HC → QAP → ILP → solve → extract back let r1 = ReduceTo::::reduce_to(&hc).expect("reduction should succeed"); @@ -157,7 +157,7 @@ fn test_hamiltoniancircuit_to_quadraticassignment_small_graphs_are_no() { (2, vec![(0, 1)]), (2, vec![(0, 0), (0, 1), (0, 1), (1, 1)]), ] { - let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges)); + let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges).unwrap()); assert!(!source.evaluate(&(0..n).collect()).unwrap().0); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let target = reduction.target_problem(); @@ -167,7 +167,9 @@ fn test_hamiltoniancircuit_to_quadraticassignment_small_graphs_are_no() { let value = target.evaluate(&best).unwrap(); assert_eq!(value, Min(Some(3))); assert!(!crate::rules::AggregateReductionResult::extract_value(&reduction, value).0); - assert!(reduction.extract_solution(&best).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &best), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } @@ -181,7 +183,8 @@ fn test_hamiltoniancircuit_to_quadraticassignment_rejects_invalid_certificates() vec![0, 0, 1, 2], vec![0, 2, 1, 3], ] { - assert!(reduction.extract_solution(&config).is_err(), "{config:?}"); + assert!(!matches!(reduction.target_problem().evaluate(&config), + Ok(value) if crate::rules::AggregateReductionResult::extract_value(&reduction, value).0)); } for value in [Min(None), Min(Some(-1)), Min(Some(1))] { assert!(!crate::rules::AggregateReductionResult::extract_value(&reduction, value).0); @@ -205,7 +208,7 @@ fn test_hamiltoniancircuit_to_quadraticassignment_all_small_graphs_and_orders() // Native loops and repeated edges must preserve the same equivalence. edges.extend((0..n).map(|v| (v, v))); edges.extend(edges.clone()); - let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges)); + let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges).unwrap()); let reduction = ReduceTo::::reduce_to(&source).unwrap(); for mut encoded in 0..n.pow(u32::try_from(n).unwrap()) { let order: Vec<_> = (0..n) @@ -224,7 +227,6 @@ fn test_hamiltoniancircuit_to_quadraticassignment_all_small_graphs_and_orders() reduction.target_problem().evaluate(&order).unwrap(), Min(None) ); - assert!(reduction.extract_solution(&order).is_err()); continue; } let missing = (0..n) @@ -240,7 +242,9 @@ fn test_hamiltoniancircuit_to_quadraticassignment_all_small_graphs_and_orders() if expected { assert_eq!(reduction.extract_solution(&order).unwrap(), order); } else { - assert!(reduction.extract_solution(&order).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &order), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } @@ -272,7 +276,7 @@ fn test_hamiltoniancircuit_to_quadraticassignment_registered_aggregate_path() { (cycle4_hc(), true), (HamiltonianCircuit::new(SimpleGraph::star(4)), false), ( - HamiltonianCircuit::new(SimpleGraph::new(2, vec![(0, 1)])), + HamiltonianCircuit::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap()), false, ), ] { diff --git a/src/unit_tests/rules/hamiltoniancircuit_ruralpostman.rs b/src/unit_tests/rules/hamiltoniancircuit_ruralpostman.rs index b13ad1365..776ced9ff 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_ruralpostman.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_ruralpostman.rs @@ -8,7 +8,7 @@ use crate::types::Min; use crate::Problem; fn triangle_hc() -> HamiltonianCircuit { - HamiltonianCircuit::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])) + HamiltonianCircuit::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()) } fn cycle4_hc() -> HamiltonianCircuit { @@ -147,3 +147,31 @@ fn test_hamiltoniancircuit_to_ruralpostman_extract_solution() { "extracted solution should be a valid Hamiltonian circuit" ); } + +#[test] +fn aggregate_distinguishes_hamiltonian_tour_cost() { + for (edges, expected) in [ + (vec![(0, 1), (1, 2), (0, 2)], true), + (vec![(0, 1), (1, 2)], false), + ] { + let source = HamiltonianCircuit::new(SimpleGraph::new(3, edges).unwrap()); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = reduction.target_problem(); + let solution = crate::solvers::ILPSolver::new().solve(target).unwrap(); + assert_eq!( + crate::rules::AggregateReductionResult::extract_value( + &reduction, + target.evaluate(&solution).unwrap() + ), + crate::types::Or(expected) + ); + if expected { + assert!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap() + .0 + ); + } + } +} diff --git a/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs b/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs index 58121a30f..b28670e2e 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs @@ -117,7 +117,7 @@ fn test_hamiltoniancircuit_to_stackercrane_prism_graph() { (1, 4), (2, 5), ]; - let source = HamiltonianCircuit::new(SimpleGraph::new(6, edges)); + let source = HamiltonianCircuit::new(SimpleGraph::new(6, edges).unwrap()); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); assert_satisfaction_round_trip_from_optimization_target( @@ -139,7 +139,7 @@ fn test_stackercrane_certificate_for_all_small_configurations() { .enumerate() .filter_map(|(i, &e)| ((mask >> i) & 1 == 1).then_some(e)) .collect(); - let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges)); + let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges).unwrap()); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let target = crate::rules::AggregateReductionResult::target_problem(&reduction); // All coordinate configurations, including repeated arc indices. @@ -157,19 +157,18 @@ fn test_stackercrane_certificate_for_all_small_configurations() { crate::rules::AggregateReductionResult::extract_value(&reduction, value).0, expected ); - let decoded = reduction.extract_solution(&config); - assert_eq!( - decoded.is_ok(), - expected, - "n={n}, mask={mask}, config={config:?}" - ); - if let Ok(order) = decoded { + if expected { + let order = reduction.extract_solution(&config).unwrap(); assert!(source.evaluate(&order).unwrap().0); } } - assert!(reduction.extract_solution(&vec![0; n + 1]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; n + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); if n > 0 { - assert!(reduction.extract_solution(&vec![n; n]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![n; n]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } diff --git a/src/unit_tests/rules/hamiltonianpath_degreeconstrainedspanningtree.rs b/src/unit_tests/rules/hamiltonianpath_degreeconstrainedspanningtree.rs index 06fc3a4a9..c6f60e889 100644 --- a/src/unit_tests/rules/hamiltonianpath_degreeconstrainedspanningtree.rs +++ b/src/unit_tests/rules/hamiltonianpath_degreeconstrainedspanningtree.rs @@ -18,7 +18,8 @@ fn edge_config(graph: &SimpleGraph, selected_edges: &[(usize, usize)]) -> Vec>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -31,7 +32,8 @@ fn test_hamiltonianpath_to_degreeconstrainedspanningtree_structure() { #[test] fn test_hamiltonianpath_to_degreeconstrainedspanningtree_closed_loop() { - let source = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 2)])); + let source = + HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 2)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); diff --git a/src/unit_tests/rules/hamiltonianpath_ilp.rs b/src/unit_tests/rules/hamiltonianpath_ilp.rs index 66a2a897e..19cd362ff 100644 --- a/src/unit_tests/rules/hamiltonianpath_ilp.rs +++ b/src/unit_tests/rules/hamiltonianpath_ilp.rs @@ -7,7 +7,7 @@ use crate::types::Or; #[test] fn test_reduction_creates_valid_ilp() { // Path P3: 0-1-2 - let problem = HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction: ReductionHamiltonianPathToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -20,7 +20,7 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_hamiltonianpath_to_ilp_closed_loop() { // Path graph: 0-1-2-3 (has Hamiltonian path) - let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); // BruteForce on source to verify feasibility let bf = BruteForce::new(); let bf_solution = bf @@ -47,7 +47,8 @@ fn test_hamiltonianpath_to_ilp_closed_loop() { #[test] fn test_hamiltonianpath_to_ilp_cycle_graph() { // C4: 0-1-2-3-0 (has multiple Hamiltonian paths) - let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)])); + let problem = + HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap()); // BruteForce on source let bf = BruteForce::new(); let bf_solution = bf @@ -69,7 +70,7 @@ fn test_hamiltonianpath_to_ilp_cycle_graph() { #[test] fn test_hamiltonianpath_to_ilp_bf_vs_ilp() { - let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let reduction: ReductionHamiltonianPathToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); @@ -78,20 +79,21 @@ fn test_hamiltonianpath_to_ilp_bf_vs_ilp() { #[test] fn test_hamiltonianpath_to_ilp_no_path() { // Disconnected graph: no Hamiltonian path - let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)])); + let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap()); let reduction: ReductionHamiltonianPathToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(reduction.target_problem()); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Disconnected graph should have no Hamiltonian path" ); } #[test] fn test_solution_extraction() { - let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let reduction: ReductionHamiltonianPathToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); diff --git a/src/unit_tests/rules/hamiltonianpath_isomorphicspanningtree.rs b/src/unit_tests/rules/hamiltonianpath_isomorphicspanningtree.rs index f0503e307..0053e338a 100644 --- a/src/unit_tests/rules/hamiltonianpath_isomorphicspanningtree.rs +++ b/src/unit_tests/rules/hamiltonianpath_isomorphicspanningtree.rs @@ -9,10 +9,9 @@ use crate::traits::Problem; #[test] fn test_hamiltonianpath_to_isomorphicspanningtree_closed_loop() { // Graph with a known Hamiltonian path: 0-1-2-3-4 plus extra edges - let source = HamiltonianPath::new(SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 3), (1, 4)], - )); + let source = HamiltonianPath::new( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 3), (1, 4)]).unwrap(), + ); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = result.target_problem(); @@ -33,7 +32,7 @@ fn test_hamiltonianpath_to_isomorphicspanningtree_closed_loop() { #[test] fn test_hamiltonianpath_to_isomorphicspanningtree_path_graph() { // Simple path graph: 0-1-2-3 (trivially has a Hamiltonian path) - let source = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let source = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); @@ -55,7 +54,8 @@ fn test_hamiltonianpath_to_isomorphicspanningtree_no_hamiltonian_path() { // So the path must be leaf-0-leaf-..., but after visiting 0 we can only // go to unvisited leaves, and from a leaf we can only go back to 0 (already visited). // Path: leaf-0-leaf is length 2, can't extend. No HP exists. - let source = HamiltonianPath::new(SimpleGraph::new(5, vec![(0, 1), (0, 2), (0, 3), (0, 4)])); + let source = + HamiltonianPath::new(SimpleGraph::new(5, vec![(0, 1), (0, 2), (0, 3), (0, 4)]).unwrap()); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let solver = BruteForce::new(); @@ -76,10 +76,9 @@ fn test_hamiltonianpath_to_isomorphicspanningtree_no_hamiltonian_path() { #[test] fn test_hamiltonianpath_to_isomorphicspanningtree_complete_graph() { // Complete graph K4: every permutation is a valid Hamiltonian path - let source = HamiltonianPath::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )); + let source = HamiltonianPath::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), + ); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); @@ -98,7 +97,7 @@ fn test_hamiltonianpath_to_isomorphicspanningtree_complete_graph() { #[test] fn test_hamiltonianpath_to_isomorphicspanningtree_small_triangle() { // Triangle: 0-1-2-0 (has Hamiltonian path, e.g. 0-1-2) - let source = HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let source = HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = result.target_problem(); diff --git a/src/unit_tests/rules/hamiltonianpathbetweentwovertices_longestpath.rs b/src/unit_tests/rules/hamiltonianpathbetweentwovertices_longestpath.rs index 4e87892d5..c63022475 100644 --- a/src/unit_tests/rules/hamiltonianpathbetweentwovertices_longestpath.rs +++ b/src/unit_tests/rules/hamiltonianpathbetweentwovertices_longestpath.rs @@ -10,10 +10,11 @@ use crate::types::One; fn test_hamiltonianpathbetweentwovertices_to_longestpath_closed_loop() { // Graph with a known Hamiltonian 0-4 path: 0-1-2-3-4 plus extra edges let source = HamiltonianPathBetweenTwoVertices::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 3), (1, 4)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 3), (1, 4)]).unwrap(), 0, 4, - ); + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = result.target_problem(); @@ -34,10 +35,11 @@ fn test_hamiltonianpathbetweentwovertices_to_longestpath_closed_loop() { fn test_hamiltonianpathbetweentwovertices_to_longestpath_path_graph() { // Simple path graph: 0-1-2-3 with s=0, t=3 (trivially has a Hamiltonian path) let source = HamiltonianPathBetweenTwoVertices::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), 0, 3, - ); + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); @@ -54,10 +56,11 @@ fn test_hamiltonianpathbetweentwovertices_to_longestpath_no_hamiltonian_path() { // No Hamiltonian path from 1 to 2 exists (vertices 3,4 are leaves // connected only to 0, so no path can visit all without revisiting 0). let source = HamiltonianPathBetweenTwoVertices::new( - SimpleGraph::new(5, vec![(0, 1), (0, 2), (0, 3), (0, 4)]), + SimpleGraph::new(5, vec![(0, 1), (0, 2), (0, 3), (0, 4)]).unwrap(), 1, 2, - ); + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let solver = BruteForce::new(); @@ -78,10 +81,11 @@ fn test_hamiltonianpathbetweentwovertices_to_longestpath_no_hamiltonian_path() { fn test_hamiltonianpathbetweentwovertices_to_longestpath_complete_graph() { // Complete graph K4 with s=0, t=3: many Hamiltonian 0-3 paths exist let source = HamiltonianPathBetweenTwoVertices::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), 0, 3, - ); + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); @@ -96,10 +100,11 @@ fn test_hamiltonianpathbetweentwovertices_to_longestpath_complete_graph() { fn test_hamiltonianpathbetweentwovertices_to_longestpath_triangle() { // Triangle: 0-1-2-0, with s=0, t=2 let source = HamiltonianPathBetweenTwoVertices::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), 0, 2, - ); + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = result.target_problem(); @@ -133,10 +138,11 @@ fn test_hamiltonian_path_extraction_for_all_small_graphs_and_endpoints() { continue; } let source = HamiltonianPathBetweenTwoVertices::new( - SimpleGraph::new(n, edges.clone()), + SimpleGraph::new(n, edges.clone()).unwrap(), start, end, - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = crate::rules::AggregateReductionResult::target_problem(&reduction); @@ -152,19 +158,14 @@ fn test_hamiltonian_path_extraction_for_all_small_graphs_and_endpoints() { .0, expected ); - let result = reduction.extract_solution(&config); - assert_eq!( - result.is_ok(), - expected, - "n={n}, graph={graph_mask}, s={start}, t={end}, config={mask}" - ); - if let Ok(order) = result { + if expected { + let order = reduction.extract_solution(&config).unwrap(); assert!(source.evaluate(&order).unwrap().0); } } - assert!(reduction - .extract_solution(&vec![false; edges.len() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; edges.len() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } diff --git a/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs b/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs index 2dc13ee79..46da7b48f 100644 --- a/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs +++ b/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs @@ -9,7 +9,7 @@ use crate::types::Min; /// Canonical issue #1023 instance: triangle {0,1,2} with leaf vertex 3 /// attached at vertex 2. Optimum deletes only the leaf edge (2,3). fn issue_instance() -> HighlyConnectedDeletion { - HighlyConnectedDeletion::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)])) + HighlyConnectedDeletion::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]).unwrap()) } #[test] @@ -85,12 +85,10 @@ fn test_highlyconnecteddeletion_to_ilp_rejects_unassigned_vertex() { let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target_solution = vec![0; reduction.target_problem().num_vars()]; - assert_eq!( - reduction - .extract_solution(&target_solution) - .unwrap_err() - .to_string(), - "vertex 0 has no selected cluster" + assert!( + !crate::traits::Problem::evaluate(reduction.target_problem(), &target_solution) + .unwrap() + .is_valid() ); } @@ -98,21 +96,24 @@ fn test_highlyconnecteddeletion_to_ilp_rejects_unassigned_vertex() { fn test_highlyconnecteddeletion_to_ilp_disconnected_no_cluster() { // Two disjoint K3's stitched by a single bridge edge. The bridge is the // only "bad" edge: removing it leaves two K3's, both highly connected. - let source = HighlyConnectedDeletion::new(SimpleGraph::new( - 6, - vec![ - // Triangle on {0,1,2}. - (0, 1), - (0, 2), - (1, 2), - // Triangle on {3,4,5}. - (3, 4), - (3, 5), - (4, 5), - // Bridge edge. - (2, 3), - ], - )); + let source = HighlyConnectedDeletion::new( + SimpleGraph::new( + 6, + vec![ + // Triangle on {0,1,2}. + (0, 1), + (0, 2), + (1, 2), + // Triangle on {3,4,5}. + (3, 4), + (3, 5), + (4, 5), + // Bridge edge. + (2, 3), + ], + ) + .unwrap(), + ); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -125,3 +126,16 @@ fn test_highlyconnecteddeletion_to_ilp_disconnected_no_cluster() { assert_bf_vs_ilp(&source, &reduction); } + +#[test] +fn subset_mask_limit_belongs_to_the_reduction() { + let source = HighlyConnectedDeletion::new(SimpleGraph::new(64, vec![]).unwrap()); + assert_eq!( + source.evaluate(&vec![]).unwrap(), + crate::types::Min(Some(0)) + ); + assert!(matches!( + ReduceTo::>::reduce_to(&source), + Err(crate::rules::ReductionError::IntegerOverflow { .. }) + )); +} diff --git a/src/unit_tests/rules/ilp_helpers.rs b/src/unit_tests/rules/ilp_helpers.rs index 9ca18265d..9d82c06dd 100644 --- a/src/unit_tests/rules/ilp_helpers.rs +++ b/src/unit_tests/rules/ilp_helpers.rs @@ -153,7 +153,7 @@ fn test_one_hot_decode_permutation() { solution[2] = 1; // item 0 -> slot 2 solution[3] = 1; // item 1 -> slot 0 solution[7] = 1; // item 2 -> slot 1 - let decoded = one_hot_decode(&solution, 3, 3, 0).unwrap(); + let decoded = one_hot_decode(&solution, 3, 3, 0); assert_eq!(decoded, vec![1, 2, 0]); // slot 0 gets item 1, slot 1 gets item 2, slot 2 gets item 0 } @@ -164,25 +164,16 @@ fn test_one_hot_decode_with_offset() { solution[7] = 1; // 5 + 2 solution[8] = 1; // 5 + 3 solution[12] = 1; // 5 + 7 - let decoded = one_hot_decode(&solution, 3, 3, 5).unwrap(); + let decoded = one_hot_decode(&solution, 3, 3, 5); assert_eq!(decoded, vec![1, 2, 0]); } -#[test] -fn test_one_hot_decode_rejects_missing_and_duplicate_items() { - assert!(one_hot_decode(&[0, 0, 0, 0], 2, 2, 0).is_err()); - assert!(one_hot_decode(&[1, 0, 1, 0], 2, 2, 0).is_err()); - assert!(one_hot_decode(&[1, 1, 0, 0], 2, 2, 0).is_err()); -} - #[test] fn test_one_hot_decode_rows_accepts_exactly_one_column_per_row() { assert_eq!( - one_hot_decode_rows(&[0, 1, 0, 1, 0, 0], 2, 3, 0).unwrap(), + one_hot_decode_rows(&[0, 1, 0, 1, 0, 0], 2, 3, 0), vec![1, 0] ); - assert!(one_hot_decode_rows(&[0, 0, 0, 1, 0, 0], 2, 3, 0).is_err()); - assert!(one_hot_decode_rows(&[1, 1, 0, 1, 0, 0], 2, 3, 0).is_err()); } #[test] diff --git a/src/unit_tests/rules/ilp_i64_ilp_bool.rs b/src/unit_tests/rules/ilp_i64_ilp_bool.rs index 75be4dc93..027b69157 100644 --- a/src/unit_tests/rules/ilp_i64_ilp_bool.rs +++ b/src/unit_tests/rules/ilp_i64_ilp_bool.rs @@ -22,8 +22,16 @@ fn integer_ilp( fn solve_via_bool(source: &ILP) -> Option<(Vec, i64)> { let reduction = ReduceTo::>::reduce_to(source).expect("reduction should succeed"); - let witness = ILPSolver::new().solve(reduction.target_problem()).ok()?; + let witness = match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => solution, + Err(crate::solvers::ILPSolveError::Infeasible) => return None, + Err(error) => panic!("ILP execution failed: {error}"), + }; let source_solution = reduction.extract_solution(&witness).unwrap(); + assert!( + source.is_feasible(&source_solution).unwrap(), + "decoded integer ILP solution must be feasible" + ); let objective = source.evaluate_objective(&source_solution).unwrap(); Some((source_solution, objective)) } @@ -39,8 +47,7 @@ fn test_ilp_i64_to_ilp_bool_closed_loop() { vec![(0, -5), (1, -6)], ObjectiveSense::Minimize, ); - let (solution, objective) = solve_via_bool(&source).unwrap(); - assert!(source.is_feasible(&solution).unwrap()); + let (_, objective) = solve_via_bool(&source).unwrap(); assert_eq!(objective, -27); } @@ -52,8 +59,7 @@ fn test_ilp_i64_to_ilp_bool_maximize() { vec![(0, 3), (1, 5)], ObjectiveSense::Maximize, ); - let (solution, objective) = solve_via_bool(&source).unwrap(); - assert!(source.is_feasible(&solution).unwrap()); + let (_, objective) = solve_via_bool(&source).unwrap(); assert_eq!(objective, 24); } @@ -98,8 +104,7 @@ fn test_ilp_i64_to_ilp_bool_equality_constraint() { vec![(0, 1)], ObjectiveSense::Minimize, ); - let (solution, objective) = solve_via_bool(&source).unwrap(); - assert!(source.is_feasible(&solution).unwrap()); + let (_, objective) = solve_via_bool(&source).unwrap(); assert_eq!(objective, 1); } @@ -130,7 +135,10 @@ fn test_ilp_i64_to_ilp_bool_infeasible() { ObjectiveSense::Minimize, ); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/ilp_casts.rs b/src/unit_tests/rules/ilp_i64_ilp_f64.rs similarity index 71% rename from src/unit_tests/rules/ilp_casts.rs rename to src/unit_tests/rules/ilp_i64_ilp_f64.rs index 2784c1f0b..5ecd98af0 100644 --- a/src/unit_tests/rules/ilp_casts.rs +++ b/src/unit_tests/rules/ilp_i64_ilp_f64.rs @@ -1,6 +1,6 @@ use super::*; use crate::models::algebraic::{IntegerVariable, ObjectiveSense}; -use crate::rules::ReductionGraph; +use crate::rules::{ReductionGraph, ReductionResult}; use crate::solvers::ILPSolver; use crate::types::MAX_EXACT_F64_INTEGER; @@ -35,7 +35,7 @@ fn test_ilp_i64_coefficients_to_f64_rejects_inexact_value() { let source = ILP::::new( 1, vec![], - vec![(0, MAX_EXACT_F64_INTEGER + 1)], + vec![(0, MAX_EXACT_F64_INTEGER + 2)], ObjectiveSense::Minimize, ) .unwrap(); @@ -47,7 +47,7 @@ fn test_ilp_i64_coefficients_to_f64_rejects_inexact_value() { } #[test] -fn test_ilp_cast_rechecks_source_feasibility() { +fn test_ilp_integer_coefficients_preserve_large_exact_constraint() { let rhs = 1_000_000_000_000_i64; let source = ILP::::with_variables( vec![IntegerVariable::nonnegative()], @@ -57,13 +57,28 @@ fn test_ilp_cast_rechecks_source_feasibility() { ) .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - let target_solution = vec![rhs + 1]; + assert_eq!(reduction.target_problem().variables(), source.variables()); + assert_eq!( + reduction.target_problem().constraints()[0].terms(), + &[(0, 1.0)] + ); + assert_eq!( + reduction.target_problem().constraints()[0].rhs(), + rhs as f64 + ); + let target_solution = vec![rhs]; assert!(reduction .target_problem() .is_feasible(&target_solution) .unwrap()); - assert!(reduction.extract_solution(&target_solution).is_err()); + assert_eq!( + reduction.extract_solution(&target_solution).unwrap(), + target_solution + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); } #[test] diff --git a/src/unit_tests/rules/ilp_qubo.rs b/src/unit_tests/rules/ilp_qubo.rs index 08300e2c8..59b817492 100644 --- a/src/unit_tests/rules/ilp_qubo.rs +++ b/src/unit_tests/rules/ilp_qubo.rs @@ -105,7 +105,7 @@ fn test_ilp_to_qubo_ge_with_slack() { let qubo = reduction.target_problem(); // 3 original + ceil(log2(3))=2 slack = 5 QUBO variables - assert_eq!(qubo.num_variables(), 5); + assert_eq!(qubo.num_variables().unwrap(), 5); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -136,7 +136,7 @@ fn test_ilp_to_qubo_le_with_slack() { let qubo = reduction.target_problem(); // 3 original + ceil(log2(3))=2 slack = 5 QUBO variables - assert_eq!(qubo.num_variables(), 5); + assert_eq!(qubo.num_variables().unwrap(), 5); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -164,7 +164,7 @@ fn test_ilp_to_qubo_structure() { let qubo = reduction.target_problem(); // Verify QUBO has appropriate structure - assert!(qubo.num_variables() >= ilp.num_vars()); + assert!(qubo.num_variables().unwrap() >= ilp.num_vars()); } #[test] @@ -215,12 +215,11 @@ fn test_ilp_qubo_all_small_rows_and_target_assignments() { // Independently detect zero squared-residual penalty. let certifies = source_value.is_valid() && energy.0.unwrap() + constant == normalized_objective; - let decoded = reduction.extract_solution(&config); - assert_eq!(decoded.is_ok(), certifies); let extracted_value = AggregateReductionResult::extract_value(&reduction, energy); assert_eq!(extracted_value.is_valid(), certifies); - if let Ok(solution) = decoded { + if certifies { + let solution = reduction.extract_solution(&config).unwrap(); assert_eq!(source.evaluate(&solution).unwrap(), source_value); assert_eq!(extracted_value, source_value); } @@ -244,9 +243,9 @@ fn test_ilp_qubo_all_small_rows_and_target_assignments() { .unwrap(); } assert_eq!(actual, expected); - assert!(reduction - .extract_solution(&vec![false; target.num_vars() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vars() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } @@ -275,7 +274,9 @@ fn test_ilp_qubo_inconsistent_rows_and_absent_aggregate() { .evaluate(&config) .unwrap(); assert!(!AggregateReductionResult::extract_value(&reduction, value).is_valid()); - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } assert!( !AggregateReductionResult::extract_value(&reduction, crate::types::Min(None)) diff --git a/src/unit_tests/rules/integralflowbundles_ilp.rs b/src/unit_tests/rules/integralflowbundles_ilp.rs index b9bea620c..bdd827a37 100644 --- a/src/unit_tests/rules/integralflowbundles_ilp.rs +++ b/src/unit_tests/rules/integralflowbundles_ilp.rs @@ -6,24 +6,26 @@ use crate::traits::Problem; fn yes_instance() -> IntegralFlowBundles { IntegralFlowBundles::new( - DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]), + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]).unwrap(), 0, 3, vec![vec![0, 1], vec![2, 5], vec![3, 4]], vec![1, 1, 1], 1, ) + .unwrap() } fn no_instance() -> IntegralFlowBundles { IntegralFlowBundles::new( - DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]), + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]).unwrap(), 0, 3, vec![vec![0, 1], vec![2, 5], vec![3, 4]], vec![1, 1, 1], 2, ) + .unwrap() } fn satisfying_config() -> Vec { @@ -99,7 +101,10 @@ fn test_integral_flow_bundles_to_ilp_unsat_instance_is_infeasible() { let problem = no_instance(); let reduction: ReductionIFBToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs b/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs index 07888d3b8..4d79f3ae5 100644 --- a/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs +++ b/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs @@ -9,13 +9,14 @@ use crate::traits::Problem; fn test_integralflowhomologousarcs_to_ilp_closed_loop() { // 4 vertices, arcs (0,1),(0,2),(1,3),(2,3), caps all 2, req 2, pair (0,1) let source = IntegralFlowHomologousArcs::new( - DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]), + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]).unwrap(), vec![2, 2, 2, 2], 0, 3, 2, vec![(0, 1)], - ); + ) + .unwrap(); // Verify source is satisfiable via brute force let direct = BruteForce::new() .solve(&source) @@ -35,13 +36,14 @@ fn test_integralflowhomologousarcs_to_ilp_closed_loop() { #[test] fn test_integralflowhomologousarcs_to_ilp_bf_vs_ilp() { let source = IntegralFlowHomologousArcs::new( - DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]), + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]).unwrap(), vec![2, 2, 2, 2], 0, 3, 2, vec![(0, 1)], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } diff --git a/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs b/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs index bf0662855..bad29802f 100644 --- a/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs +++ b/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs @@ -9,13 +9,14 @@ use crate::traits::Problem; fn test_integralflowwithmultipliers_to_ilp_closed_loop() { // 4 vertices, arcs (0,1),(0,2),(1,3),(2,3), multipliers all 1, caps all 2, req 2 let source = IntegralFlowWithMultipliers::new( - DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]), + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]).unwrap(), 0, 3, vec![1, 1, 1, 1], vec![2, 2, 2, 2], 2, - ); + ) + .unwrap(); let direct = BruteForce::new() .solve(&source) .unwrap() @@ -34,13 +35,14 @@ fn test_integralflowwithmultipliers_to_ilp_closed_loop() { #[test] fn test_integralflowwithmultipliers_to_ilp_bf_vs_ilp() { let source = IntegralFlowWithMultipliers::new( - DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]), + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]).unwrap(), 0, 3, vec![1, 1, 1, 1], vec![2, 2, 2, 2], 2, - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } diff --git a/src/unit_tests/rules/isomorphicspanningtree_ilp.rs b/src/unit_tests/rules/isomorphicspanningtree_ilp.rs index 4f90f2ee2..cfd55aabe 100644 --- a/src/unit_tests/rules/isomorphicspanningtree_ilp.rs +++ b/src/unit_tests/rules/isomorphicspanningtree_ilp.rs @@ -9,8 +9,8 @@ use crate::types::Or; #[test] fn test_reduction_creates_valid_ilp() { // K3, path tree - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = IsomorphicSpanningTree::new(graph, tree); let reduction: ReductionISTToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -23,8 +23,8 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_isomorphicspanningtree_to_ilp_closed_loop() { - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); - let tree = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); + let tree = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(); let problem = IsomorphicSpanningTree::new(graph, tree); let reduction: ReductionISTToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -34,8 +34,8 @@ fn test_isomorphicspanningtree_to_ilp_closed_loop() { #[test] fn test_isomorphicspanningtree_to_ilp_bf_vs_ilp() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]); - let tree = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]).unwrap(); + let tree = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let problem = IsomorphicSpanningTree::new(graph, tree); let bf = BruteForce::new(); @@ -58,8 +58,8 @@ fn test_isomorphicspanningtree_to_ilp_bf_vs_ilp() { #[test] fn test_solution_extraction() { // K3 with path tree - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let tree = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = IsomorphicSpanningTree::new(graph, tree); let reduction: ReductionISTToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); diff --git a/src/unit_tests/rules/kclique_balancedcompletebipartitesubgraph.rs b/src/unit_tests/rules/kclique_balancedcompletebipartitesubgraph.rs index fb22fc12f..ebb8db054 100644 --- a/src/unit_tests/rules/kclique_balancedcompletebipartitesubgraph.rs +++ b/src/unit_tests/rules/kclique_balancedcompletebipartitesubgraph.rs @@ -8,7 +8,11 @@ use crate::types::Or; fn test_kclique_to_balancedcompletebipartitesubgraph_closed_loop() { // 4-vertex graph with edges {0,1}, {0,2}, {1,2}, {2,3}, k=3 // Known 3-clique: {0, 1, 2} - let source = KClique::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]), 3); + let source = KClique::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]).unwrap(), + 3, + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -32,9 +36,10 @@ fn test_kclique_to_balancedcompletebipartitesubgraph_closed_loop() { fn test_kclique_to_bcbs_complete_graph() { // K4 graph, k=3 -> should find a 3-clique let source = KClique::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), 3, - ); + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -55,7 +60,11 @@ fn test_kclique_to_bcbs_complete_graph() { #[test] fn test_kclique_to_bcbs_no_clique() { // Path graph: 0-1-2-3, k=3 -> no 3-clique exists - let source = KClique::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), 3); + let source = KClique::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), + 3, + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -81,7 +90,7 @@ fn test_kclique_to_bcbs_no_clique() { #[test] fn test_kclique_to_bcbs_k_equals_2() { // k=2 means we need an edge - let source = KClique::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), 2); + let source = KClique::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(), 2).unwrap(); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -104,7 +113,7 @@ fn test_kclique_to_bcbs_k_equals_2() { #[test] fn test_kclique_to_bcbs_k_equals_1() { // k=1: any graph has a 1-clique (single vertex) - let source = KClique::new(SimpleGraph::new(3, vec![(0, 1)]), 1); + let source = KClique::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), 1).unwrap(); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -140,9 +149,11 @@ fn test_kclique_to_bcbs_bipartite_counterexample() { (2, 4), (2, 5), ], - ), + ) + .unwrap(), 3, - ); + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/unit_tests/rules/kclique_conjunctivebooleanquery.rs b/src/unit_tests/rules/kclique_conjunctivebooleanquery.rs index 63591a562..be81bbd9e 100644 --- a/src/unit_tests/rules/kclique_conjunctivebooleanquery.rs +++ b/src/unit_tests/rules/kclique_conjunctivebooleanquery.rs @@ -11,8 +11,8 @@ use crate::types::Or; #[test] fn test_kclique_to_conjunctivebooleanquery_closed_loop() { // Triangle graph (0,1,2) plus extra edges, k=3 - let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 2), (2, 3), (3, 4)]); - let problem = KClique::new(graph, 3); + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 2), (2, 3), (3, 4)]).unwrap(); + let problem = KClique::new(graph, 3).unwrap(); let reduction: ReductionKCliqueToCBQ = ReduceTo::::reduce_to(&problem).expect("reduction should succeed"); @@ -26,8 +26,8 @@ fn test_kclique_to_conjunctivebooleanquery_closed_loop() { #[test] fn test_reduction_structure() { // Complete graph K4, k=3 - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); - let problem = KClique::new(graph, 3); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); + let problem = KClique::new(graph, 3).unwrap(); let reduction: ReductionKCliqueToCBQ = ReduceTo::::reduce_to(&problem).expect("reduction should succeed"); let cbq = reduction.target_problem(); @@ -48,8 +48,8 @@ fn test_reduction_structure() { #[test] fn test_no_clique_infeasible() { // Path graph 0-1-2, k=3 → no triangle - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = KClique::new(graph, 3); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = KClique::new(graph, 3).unwrap(); let reduction: ReductionKCliqueToCBQ = ReduceTo::::reduce_to(&problem).expect("reduction should succeed"); @@ -63,8 +63,8 @@ fn test_no_clique_infeasible() { #[test] fn test_solution_extraction() { // Triangle graph, k=3 - let graph = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); - let problem = KClique::new(graph, 3); + let graph = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); + let problem = KClique::new(graph, 3).unwrap(); let reduction: ReductionKCliqueToCBQ = ReduceTo::::reduce_to(&problem).expect("reduction should succeed"); @@ -82,8 +82,8 @@ fn test_solution_extraction() { #[test] fn test_trivial_k1() { // Any graph with at least 1 vertex, k=1 → always feasible - let graph = SimpleGraph::new(3, vec![(0, 1)]); - let problem = KClique::new(graph, 1); + let graph = SimpleGraph::new(3, vec![(0, 1)]).unwrap(); + let problem = KClique::new(graph, 1).unwrap(); let reduction: ReductionKCliqueToCBQ = ReduceTo::::reduce_to(&problem).expect("reduction should succeed"); let cbq = reduction.target_problem(); diff --git a/src/unit_tests/rules/kclique_ilp.rs b/src/unit_tests/rules/kclique_ilp.rs index 5e5b86e60..cc579450f 100644 --- a/src/unit_tests/rules/kclique_ilp.rs +++ b/src/unit_tests/rules/kclique_ilp.rs @@ -7,8 +7,8 @@ use crate::types::Or; #[test] fn test_reduction_creates_valid_ilp() { // Triangle graph, k=3 - let graph = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); - let problem = KClique::new(graph, 3); + let graph = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); + let problem = KClique::new(graph, 3).unwrap(); let reduction: ReductionKCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -21,8 +21,8 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_kclique_to_ilp_bf_vs_ilp() { // K4 graph, k=3 → has 3-clique - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); - let problem = KClique::new(graph, 3); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); + let problem = KClique::new(graph, 3).unwrap(); let reduction: ReductionKCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -40,8 +40,8 @@ fn test_kclique_to_ilp_bf_vs_ilp() { #[test] fn test_solution_extraction() { - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); - let problem = KClique::new(graph, 3); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); + let problem = KClique::new(graph, 3).unwrap(); let reduction: ReductionKCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); @@ -57,8 +57,8 @@ fn test_solution_extraction() { #[test] fn test_kclique_to_ilp_trivial() { // Empty graph (no edges), k=1 → trivially feasible (any single vertex is a 1-clique) - let graph = SimpleGraph::new(3, vec![]); - let problem = KClique::new(graph, 1); + let graph = SimpleGraph::new(3, vec![]).unwrap(); + let problem = KClique::new(graph, 1).unwrap(); let reduction: ReductionKCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/kclique_subgraphisomorphism.rs b/src/unit_tests/rules/kclique_subgraphisomorphism.rs index 7ba1863be..8fdc94924 100644 --- a/src/unit_tests/rules/kclique_subgraphisomorphism.rs +++ b/src/unit_tests/rules/kclique_subgraphisomorphism.rs @@ -9,9 +9,10 @@ fn test_kclique_to_subgraphisomorphism_closed_loop() { // 5-vertex graph with a known 3-clique on vertices {2, 3, 4} // Edges: 0-1, 0-2, 1-3, 2-3, 2-4, 3-4 let source = KClique::new( - SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(), 3, - ); + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -34,9 +35,10 @@ fn test_kclique_to_subgraphisomorphism_closed_loop() { fn test_kclique_to_subgraphisomorphism_complete_graph() { // K4 graph, k=3 -> should find a 3-clique let source = KClique::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), 3, - ); + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -58,7 +60,11 @@ fn test_kclique_to_subgraphisomorphism_complete_graph() { #[test] fn test_kclique_to_subgraphisomorphism_no_clique() { // Path graph: 0-1-2-3, k=3 -> no 3-clique exists - let source = KClique::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), 3); + let source = KClique::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), + 3, + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -81,7 +87,7 @@ fn test_kclique_to_subgraphisomorphism_no_clique() { #[test] fn test_kclique_to_subgraphisomorphism_k_equals_1() { // Any non-empty graph has a 1-clique (single vertex) - let source = KClique::new(SimpleGraph::new(3, vec![(0, 1)]), 1); + let source = KClique::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), 1).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -103,7 +109,7 @@ fn test_kclique_to_subgraphisomorphism_k_equals_1() { #[test] fn test_kclique_to_subgraphisomorphism_k_equals_2() { // k=2 means we need an edge - let source = KClique::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), 2); + let source = KClique::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(), 2).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/unit_tests/rules/kcoloring_bicliquecover.rs b/src/unit_tests/rules/kcoloring_bicliquecover.rs index 9d86c589b..dc540a9ae 100644 --- a/src/unit_tests/rules/kcoloring_bicliquecover.rs +++ b/src/unit_tests/rules/kcoloring_bicliquecover.rs @@ -16,7 +16,7 @@ fn cell(config: &[Vec], vertex: usize, biclique: usize) -> bool { #[test] fn test_kcoloring_to_bicliquecover_closed_loop_trivial() { // Single isolated vertex with q = 1: trivially 1-colorable. - let source = KColoring::::with_k(SimpleGraph::new(1, vec![]), 1); + let source = KColoring::::with_k(SimpleGraph::new(1, vec![]).unwrap(), 1); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -40,7 +40,7 @@ fn test_kcoloring_to_bicliquecover_closed_loop_trivial() { #[test] fn test_kcoloring_to_bicliquecover_structure_path() { // n = 3, m = 2 (path 0-1-2), q = 2. - let source = KColoring::::with_k(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 2); + let source = KColoring::::with_k(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 2); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -63,7 +63,7 @@ fn test_kcoloring_to_bicliquecover_structure_path() { fn test_kcoloring_to_bicliquecover_structure_clique() { // K_4: n = 4, m = 6, q = 3. let source = KColoring::::with_k( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), 3, ); let reduction = @@ -90,7 +90,7 @@ fn test_kcoloring_to_bicliquecover_structure_clique() { fn test_kcoloring_to_bicliquecover_forward_witness_path_q2() { // P_3 with the obvious 2-coloring (0, 1, 0). Independent sets are // {0, 2} (color 0) and {1} (color 1). - let source = KColoring::::with_k(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 2); + let source = KColoring::::with_k(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 2); let coloring = vec![0usize, 1, 0]; assert!(source.is_valid_solution(&coloring)); @@ -110,8 +110,10 @@ fn test_kcoloring_to_bicliquecover_forward_witness_path_q2() { /// 4-cycle is bipartite, with a canonical 2-coloring (0,1,0,1). #[test] fn test_kcoloring_to_bicliquecover_forward_witness_cycle_q2() { - let source = - KColoring::::with_k(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]), 2); + let source = KColoring::::with_k( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]).unwrap(), + 2, + ); let coloring = vec![0usize, 1, 0, 1]; assert!(source.is_valid_solution(&coloring)); @@ -131,7 +133,7 @@ fn test_kcoloring_to_bicliquecover_forward_witness_cycle_q2() { #[test] fn test_kcoloring_to_bicliquecover_rejects_adjacent_grouping() { // P_2 with q = 2; edge (0, 1) means vertices 0 and 1 are adjacent. - let source = KColoring::::with_k(SimpleGraph::new(2, vec![(0, 1)]), 2); + let source = KColoring::::with_k(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 2); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -178,7 +180,10 @@ fn test_kcoloring_to_bicliquecover_rejects_adjacent_grouping() { #[test] fn test_kcoloring_to_bicliquecover_extract_solution_on_forward_witness() { // Triangle K_3 with q = 3: each vertex must have its own color. - let source = KColoring::::with_k(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), 3); + let source = KColoring::::with_k( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), + 3, + ); let coloring = vec![0usize, 1, 2]; assert!(source.is_valid_solution(&coloring)); @@ -204,7 +209,7 @@ fn test_kcoloring_to_bicliquecover_extract_solution_on_forward_witness() { #[test] fn test_kcoloring_to_bicliquecover_explicit_edges_p2() { // P_2: n = 2, m = 1, edge (0,1), q = 2. - let source = KColoring::::with_k(SimpleGraph::new(2, vec![(0, 1)]), 2); + let source = KColoring::::with_k(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 2); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -230,7 +235,7 @@ fn test_kcoloring_to_bicliquecover_explicit_edges_p2() { /// biclique cover witness yields a single-vertex coloring of color 0. #[test] fn test_kcoloring_to_bicliquecover_extract_trivial_layout() { - let source = KColoring::::with_k(SimpleGraph::new(1, vec![]), 1); + let source = KColoring::::with_k(SimpleGraph::new(1, vec![]).unwrap(), 1); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -255,14 +260,16 @@ fn test_kcoloring_to_bicliquecover_extract_trivial_layout() { fn test_kcoloring_to_bicliquecover_native_loops_are_infeasible() { for n in [1, 4] { for q in [0, 1, usize::MAX] { - let source = KColoring::::with_k(SimpleGraph::new(n, vec![(0, 0)]), q); + let source = KColoring::::with_k(SimpleGraph::new(n, vec![(0, 0)]).unwrap(), q); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let target = reduction.target_problem(); assert_eq!(target.k(), 0); assert_eq!(target.graph().left_edges(), &[(0, 0)]); assert!(target.evaluate(&vec![]).unwrap().0.is_none()); assert!(BruteForce::new().solve(target).unwrap().is_none()); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); if q == 0 { assert!(source.evaluate(&vec![0; n]).is_err()); } else { @@ -276,7 +283,7 @@ fn test_kcoloring_to_bicliquecover_native_loops_are_infeasible() { fn test_kcoloring_to_bicliquecover_normalizes_all_color_counts() { for n in 0..=3 { for q in [0, 1, 2, 3, 4, usize::MAX] { - let source = KColoring::::with_k(SimpleGraph::new(n, vec![]), q); + let source = KColoring::::with_k(SimpleGraph::new(n, vec![]).unwrap(), q); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let target = reduction.target_problem(); assert_eq!(target.k(), n + q.min(n)); @@ -295,7 +302,7 @@ fn test_kcoloring_to_bicliquecover_normalizes_all_color_counts() { #[test] fn test_kcoloring_to_bicliquecover_rejects_invalid_certificates() { - let source = KColoring::::with_k(SimpleGraph::new(2, vec![(0, 1)]), 2); + let source = KColoring::::with_k(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 2); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let target = reduction.target_problem(); for invalid in [ @@ -304,7 +311,9 @@ fn test_kcoloring_to_bicliquecover_rejects_invalid_certificates() { vec![vec![true; 8]; 4], vec![vec![false; 8]; 4], ] { - assert!(reduction.extract_solution(&invalid).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &invalid), Ok(value) if { value.is_valid() }) + ); } let valid = forward_witness(&source, &[0, 1]); assert!(target.evaluate(&valid).unwrap().0.is_some()); @@ -320,9 +329,11 @@ fn test_kcoloring_to_bicliquecover_rejects_invalid_certificates() { #[test] fn test_kcoloring_to_bicliquecover_repeated_reversed_edges() { - let simple = KColoring::::with_k(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 2); - let repeated = - KColoring::::with_k(SimpleGraph::new(3, vec![(0, 1), (1, 0), (1, 2), (0, 1)]), 2); + let simple = KColoring::::with_k(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 2); + let repeated = KColoring::::with_k( + SimpleGraph::new(3, vec![(0, 1), (1, 0), (1, 2), (0, 1)]).unwrap(), + 2, + ); let a = ReduceTo::::reduce_to(&simple).unwrap(); let b = ReduceTo::::reduce_to(&repeated).unwrap(); assert_eq!( @@ -341,7 +352,7 @@ fn test_kcoloring_to_bicliquecover_repeated_reversed_edges() { #[test] fn test_kcoloring_to_bicliquecover_all_single_vertex_target_configs() { for q in 0..=1 { - let source = KColoring::::with_k(SimpleGraph::new(1, vec![]), q); + let source = KColoring::::with_k(SimpleGraph::new(1, vec![]).unwrap(), q); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let target = reduction.target_problem(); let bits = target.k() * target.num_vertices(); @@ -355,9 +366,8 @@ fn test_kcoloring_to_bicliquecover_all_single_vertex_target_configs() { }) .collect(); let value = target.evaluate(&config).unwrap(); - let decoded = reduction.extract_solution(&config); - assert_eq!(decoded.is_ok(), value.0.is_some()); - if let Ok(coloring) = decoded { + if value.0.is_some() { + let coloring = reduction.extract_solution(&config).unwrap(); feasible = true; assert!(source.evaluate(&coloring).unwrap().0); } diff --git a/src/unit_tests/rules/kcoloring_clustering.rs b/src/unit_tests/rules/kcoloring_clustering.rs index 36473f489..8090df181 100644 --- a/src/unit_tests/rules/kcoloring_clustering.rs +++ b/src/unit_tests/rules/kcoloring_clustering.rs @@ -18,7 +18,8 @@ fn test_kcoloring_to_clustering_closed_loop() { #[test] fn test_kcoloring_to_clustering_distance_matrix() { - let source = KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let source = + KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -38,7 +39,7 @@ fn test_kcoloring_to_clustering_distance_matrix() { #[test] fn test_kcoloring_to_clustering_extract_solution_identity() { - let source = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let source = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let config = vec![0, 1, 0]; @@ -57,7 +58,7 @@ fn test_kcoloring_to_clustering_unsat_preserved() { #[test] fn test_kcoloring_to_clustering_empty_graph() { - let source = KColoring::::new(SimpleGraph::new(0, vec![])); + let source = KColoring::::new(SimpleGraph::new(0, vec![]).unwrap()); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/unit_tests/rules/kcoloring_partitionintocliques.rs b/src/unit_tests/rules/kcoloring_partitionintocliques.rs index c05b094c7..8c8d75675 100644 --- a/src/unit_tests/rules/kcoloring_partitionintocliques.rs +++ b/src/unit_tests/rules/kcoloring_partitionintocliques.rs @@ -7,7 +7,7 @@ use crate::variant::KN; #[test] fn test_kcoloring_to_partitionintocliques_closed_loop() { let source = KColoring::::with_k( - SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(), 3, ); let reduction = ReduceTo::>::reduce_to(&source) @@ -22,7 +22,10 @@ fn test_kcoloring_to_partitionintocliques_closed_loop() { #[test] fn test_kcoloring_to_partitionintocliques_complement_structure() { - let source = KColoring::::with_k(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), 2); + let source = KColoring::::with_k( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), + 2, + ); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -37,7 +40,7 @@ fn test_kcoloring_to_partitionintocliques_complement_structure() { #[test] fn test_kcoloring_to_partitionintocliques_extract_solution_identity() { - let source = KColoring::::with_k(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 2); + let source = KColoring::::with_k(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 2); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let config = vec![0, 1, 0]; @@ -47,7 +50,10 @@ fn test_kcoloring_to_partitionintocliques_extract_solution_identity() { #[test] fn test_kcoloring_to_partitionintocliques_unsat_preserved() { - let source = KColoring::::with_k(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), 2); + let source = KColoring::::with_k( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), + 2, + ); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let solver = BruteForce::new(); diff --git a/src/unit_tests/rules/kcoloring_twodimensionalconsecutivesets.rs b/src/unit_tests/rules/kcoloring_twodimensionalconsecutivesets.rs index 46338d9c3..08e5b0f73 100644 --- a/src/unit_tests/rules/kcoloring_twodimensionalconsecutivesets.rs +++ b/src/unit_tests/rules/kcoloring_twodimensionalconsecutivesets.rs @@ -11,7 +11,8 @@ use crate::variant::K3; #[test] fn test_kcoloring_to_twodimensionalconsecutivesets_closed_loop() { // Triangle graph: 3-colorable - let source = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let source = + KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); @@ -25,7 +26,8 @@ fn test_kcoloring_to_twodimensionalconsecutivesets_closed_loop() { #[test] fn test_kcoloring_to_tdcs_target_structure() { // Graph with 4 vertices and 3 edges: path 0-1-2-3 - let source = KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let source = + KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -46,10 +48,9 @@ fn test_kcoloring_to_tdcs_non_3colorable() { // Use K_3 + edge to make a non-3-colorable subgraph: vertex 0 connected to 1, 2; // vertex 1 connected to 2; all three connected to vertex 3 // This is K4 but we only check source side (target brute-force too slow). - let source = KColoring::::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )); + let source = KColoring::::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), + ); let solver = BruteForce::new(); let source_solutions = solver.find_all_witnesses(&source).unwrap(); @@ -66,7 +67,7 @@ fn test_kcoloring_to_tdcs_non_3colorable() { #[test] fn test_kcoloring_to_tdcs_bipartite() { // Path 0-1-2: bipartite, 2-colorable (hence 3-colorable) - let source = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let source = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); @@ -80,7 +81,7 @@ fn test_kcoloring_to_tdcs_bipartite() { #[test] fn test_kcoloring_to_tdcs_single_edge() { // Single edge: trivially 3-colorable - let source = KColoring::::new(SimpleGraph::new(2, vec![(0, 1)])); + let source = KColoring::::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap()); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -98,7 +99,8 @@ fn test_kcoloring_to_tdcs_single_edge() { #[test] fn test_kcoloring_to_tdcs_extract_solution_valid() { // Triangle: verify extracted coloring is valid - let source = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let source = + KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); @@ -135,7 +137,7 @@ fn test_kcoloring_to_tdcs_empty_graph_has_a_target_witness() { #[test] fn test_kcoloring_to_tdcs_native_loops_are_no() { for n in [1, 5] { - let source = KColoring::::new(SimpleGraph::new(n, vec![(0, 0)])); + let source = KColoring::::new(SimpleGraph::new(n, vec![(0, 0)]).unwrap()); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let target = reduction.target_problem(); assert_eq!(target.alphabet_size(), 3); @@ -145,7 +147,9 @@ fn test_kcoloring_to_tdcs_native_loops_are_no() { for a in 0..3 { for b in 0..3 { for c in 0..3 { - assert!(reduction.extract_solution(&vec![a, b, c]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![a, b, c]), Ok(value) if { value.is_valid() }) + ); } } } @@ -154,10 +158,12 @@ fn test_kcoloring_to_tdcs_native_loops_are_no() { #[test] fn test_kcoloring_to_tdcs_rejects_noncertificates() { - let source = KColoring::::new(SimpleGraph::new(2, vec![(0, 1)])); + let source = KColoring::::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap()); let reduction = ReduceTo::::reduce_to(&source).unwrap(); for config in [vec![], vec![0, 1], vec![0, 1, 3], vec![0, 0, 0]] { - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } } @@ -178,7 +184,7 @@ fn test_kcoloring_to_tdcs_many_groups_gaps_and_repeated_edges() { vec![0, 1], ), ] { - let source = KColoring::::new(SimpleGraph::new(n, edges)); + let source = KColoring::::new(SimpleGraph::new(n, edges).unwrap()); let reduction = ReduceTo::::reduce_to(&source).unwrap(); assert!(reduction.target_problem().evaluate(&grouping).unwrap().0); let coloring = reduction.extract_solution(&grouping).unwrap(); @@ -200,7 +206,7 @@ fn test_kcoloring_to_tdcs_all_tiny_graphs_and_target_assignments() { .filter(|(i, _)| mask & (1 << i) != 0) .map(|(_, &e)| e) .collect(); - let source = KColoring::::new(SimpleGraph::new(n, edges)); + let source = KColoring::::new(SimpleGraph::new(n, edges).unwrap()); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let target = reduction.target_problem(); let size = target.alphabet_size(); @@ -214,9 +220,8 @@ fn test_kcoloring_to_tdcs_all_tiny_graphs_and_target_assignments() { }) .collect(); let feasible = target.evaluate(&grouping).unwrap().0; - let extracted = reduction.extract_solution(&grouping); - assert_eq!(extracted.is_ok(), feasible); - if let Ok(coloring) = extracted { + if feasible { + let coloring = reduction.extract_solution(&grouping).unwrap(); assert!(source.evaluate(&coloring).unwrap().0); target_yes = true; } diff --git a/src/unit_tests/rules/knapsack_ilp.rs b/src/unit_tests/rules/knapsack_ilp.rs index bf72d4cff..04ab98d2a 100644 --- a/src/unit_tests/rules/knapsack_ilp.rs +++ b/src/unit_tests/rules/knapsack_ilp.rs @@ -6,7 +6,7 @@ use crate::traits::Problem; #[test] fn test_knapsack_to_ilp_closed_loop() { - let knapsack = Knapsack::new(vec![1, 3, 4, 5], vec![1, 4, 5, 7], 7); + let knapsack = Knapsack::new(vec![1, 3, 4, 5], vec![1, 4, 5, 7], 7).unwrap(); let reduction = ReduceTo::>::reduce_to(&knapsack).expect("reduction should succeed"); assert_bf_vs_ilp(&knapsack, &reduction); @@ -20,7 +20,7 @@ fn test_knapsack_to_ilp_closed_loop() { #[test] fn test_knapsack_to_ilp_bf_vs_ilp() { - let knapsack = Knapsack::new(vec![1, 3, 4, 5], vec![1, 4, 5, 7], 7); + let knapsack = Knapsack::new(vec![1, 3, 4, 5], vec![1, 4, 5, 7], 7).unwrap(); let reduction = ReduceTo::>::reduce_to(&knapsack).expect("reduction should succeed"); let bf_solutions = BruteForce::new().find_all_witnesses(&knapsack).unwrap(); @@ -38,7 +38,7 @@ fn test_knapsack_to_ilp_bf_vs_ilp() { #[test] fn test_knapsack_to_ilp_structure() { - let knapsack = Knapsack::new(vec![1, 3, 4, 5], vec![1, 4, 5, 7], 7); + let knapsack = Knapsack::new(vec![1, 3, 4, 5], vec![1, 4, 5, 7], 7).unwrap(); let reduction = ReduceTo::>::reduce_to(&knapsack).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -55,7 +55,7 @@ fn test_knapsack_to_ilp_structure() { #[test] fn test_knapsack_to_ilp_zero_capacity() { - let knapsack = Knapsack::new(vec![2, 3], vec![5, 7], 0); + let knapsack = Knapsack::new(vec![2, 3], vec![5, 7], 0).unwrap(); let reduction = ReduceTo::>::reduce_to(&knapsack).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() @@ -67,7 +67,7 @@ fn test_knapsack_to_ilp_zero_capacity() { #[test] fn test_knapsack_to_ilp_empty_instance() { - let knapsack = Knapsack::new(vec![], vec![], 0); + let knapsack = Knapsack::new(vec![], vec![], 0).unwrap(); let reduction = ReduceTo::>::reduce_to(&knapsack).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -88,7 +88,7 @@ fn test_knapsack_to_ilp_empty_instance() { #[test] fn test_knapsack_to_ilp_preserves_large_exact_weight() { let weight = crate::types::MAX_EXACT_F64_INTEGER + 1; - let knapsack = Knapsack::new(vec![weight], vec![1], weight); + let knapsack = Knapsack::new(vec![weight], vec![1], weight).unwrap(); let reduction = ReduceTo::>::reduce_to(&knapsack).unwrap(); let constraint = &reduction.target_problem().constraints()[0]; diff --git a/src/unit_tests/rules/knapsack_qubo.rs b/src/unit_tests/rules/knapsack_qubo.rs index 29ff7203c..7c1a51840 100644 --- a/src/unit_tests/rules/knapsack_qubo.rs +++ b/src/unit_tests/rules/knapsack_qubo.rs @@ -5,7 +5,7 @@ use crate::traits::Problem; #[test] fn test_knapsack_to_qubo_closed_loop() { - let knapsack = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + let knapsack = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7).unwrap(); let reduction = ReduceTo::>::reduce_to(&knapsack).expect("reduction should succeed"); let qubo = reduction.target_problem(); @@ -20,7 +20,7 @@ fn test_knapsack_to_qubo_closed_loop() { #[test] fn test_knapsack_to_qubo_single_item() { - let knapsack = Knapsack::new(vec![1], vec![1], 1); + let knapsack = Knapsack::new(vec![1], vec![1], 1).unwrap(); let reduction = ReduceTo::>::reduce_to(&knapsack).expect("reduction should succeed"); let qubo = reduction.target_problem(); @@ -34,7 +34,7 @@ fn test_knapsack_to_qubo_single_item() { #[test] fn test_knapsack_to_qubo_infeasible_rejected() { - let knapsack = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + let knapsack = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7).unwrap(); let reduction = ReduceTo::>::reduce_to(&knapsack).expect("reduction should succeed"); let qubo = reduction.target_problem(); @@ -53,7 +53,7 @@ fn test_knapsack_to_qubo_infeasible_rejected() { #[test] fn test_knapsack_to_qubo_empty() { - let knapsack = Knapsack::new(vec![1, 2], vec![3, 4], 0); + let knapsack = Knapsack::new(vec![1, 2], vec![3, 4], 0).unwrap(); let reduction = ReduceTo::>::reduce_to(&knapsack).expect("reduction should succeed"); let qubo = reduction.target_problem(); @@ -77,6 +77,6 @@ fn test_knapsack_to_qubo_canonical_example_spec() { assert_eq!(example.source.problem, "Knapsack"); assert_eq!(example.target.problem, "QUBO"); assert_eq!(example.source.instance["capacity"], 7); - assert_eq!(example.target.instance["num_vars"], 7); + assert_eq!(example.target.instance["matrix"]["nrows"], 7); assert!(!example.solutions.is_empty()); } diff --git a/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs b/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs index d12fc33e7..803d7cba1 100644 --- a/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs +++ b/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs @@ -35,7 +35,9 @@ fn test_ksatisfiability_to_acyclicpartition_closed_loop() { .0 ); } else { - assert!(reduction.extract_solution(&labels).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &labels), Ok(value) if { value.is_valid() }) + ); } } assert_eq!(count, 3); @@ -53,7 +55,9 @@ fn test_acyclicpartition_extraction_rejects_invalid_targets() { vec![0; 9], vec![2, 1, 1, 0, 0, 1, 1, 0, 1], ] { - assert!(reduction.extract_solution(&labels).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &labels), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/ksatisfiability_bicliquecover.rs b/src/unit_tests/rules/ksatisfiability_bicliquecover.rs index 9c6b4b2c9..22e336c8a 100644 --- a/src/unit_tests/rules/ksatisfiability_bicliquecover.rs +++ b/src/unit_tests/rules/ksatisfiability_bicliquecover.rs @@ -125,7 +125,9 @@ fn test_ksatisfiability_to_bicliquecover_rejects_invalid_covers() { vec![vec![true; target.num_vertices()]; target.k()], vec![vec![false; target.num_vertices() - 1]; target.k()], ] { - assert!(reduction.extract_solution(&invalid).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &invalid), Ok(value) if { value.is_valid() }) + ); } } @@ -252,7 +254,9 @@ fn test_ksatisfiability_to_bicliquecover_empty_conjunction_and_clause() { .unwrap() .0 .is_none()); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); assert!(!no.evaluate(&vec![false; n]).unwrap().0); } } diff --git a/src/unit_tests/rules/ksatisfiability_cyclicordering.rs b/src/unit_tests/rules/ksatisfiability_cyclicordering.rs index 75c1ab6f7..c8e312447 100644 --- a/src/unit_tests/rules/ksatisfiability_cyclicordering.rs +++ b/src/unit_tests/rules/ksatisfiability_cyclicordering.rs @@ -271,7 +271,9 @@ fn empty_formula_and_empty_clause_have_opposite_fixed_targets() { vec![2, 1, 0], ] { assert!(!reduction.target_problem().evaluate(&config).unwrap().0); - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } } } @@ -293,7 +295,9 @@ fn reject_invalid_orderings_and_accept_every_rotation() { ); } for config in [vec![], vec![0; n], vec![n; n], (0..n).collect()] { - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs b/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs index 6ad979fc3..908cf2509 100644 --- a/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs +++ b/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs @@ -40,9 +40,17 @@ fn solve_target_via_ilp( problem: &crate::models::graph::DirectedTwoCommodityIntegralFlow, ) -> Option> { let reduction = ReduceTo::>::reduce_to(problem).expect("reduction should succeed"); - let ilp_solution = ILPSolver::new().solve(reduction.target_problem()).ok()?; + let ilp_solution = match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => solution, + Err(crate::solvers::ILPSolveError::Infeasible) => return None, + Err(error) => panic!("ILP execution failed: {error}"), + }; let extracted = reduction.extract_solution(&ilp_solution).unwrap(); - problem.evaluate(&extracted).unwrap().0.then_some(extracted) + assert!( + problem.evaluate(&extracted).unwrap().0, + "decoded flow must be feasible" + ); + Some(extracted) } #[test] diff --git a/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs b/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs index ef7624f3d..526812d9e 100644 --- a/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs +++ b/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs @@ -170,8 +170,9 @@ fn test_ksatisfiability_to_feasible_register_assignment_unsatisfiable_instance() let fra_to_ilp = ReduceTo::>::reduce_to(reduction.target_problem()) .expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(fra_to_ilp.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(fra_to_ilp.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "an unsatisfiable source formula should yield an infeasible FRA instance" ); } @@ -193,7 +194,9 @@ fn native_empty_clause_is_infeasible_and_empty_conjunction_is_feasible() { vec![2, 1, 0], ] { assert!(!reduction.target_problem().evaluate(&config).unwrap().0); - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } let source = KSatisfiability::::new(num_vars, vec![]); let reduction = ReduceTo::::reduce_to(&source).unwrap(); @@ -282,7 +285,9 @@ fn invalid_realizations_are_rejected() { let reduction = ReduceTo::::reduce_to(&source).unwrap(); let n = reduction.target_problem().num_vertices(); for config in [vec![], vec![n; n], vec![0; n], (0..n).collect()] { - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/ksatisfiability_kclique.rs b/src/unit_tests/rules/ksatisfiability_kclique.rs index 0e0bf789b..157f25686 100644 --- a/src/unit_tests/rules/ksatisfiability_kclique.rs +++ b/src/unit_tests/rules/ksatisfiability_kclique.rs @@ -147,7 +147,9 @@ fn test_kclique_all_two_clause_formulas_and_target_selections() { .0 ); } else { - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { value.is_valid() }) + ); } } assert_eq!(source_yes, target_yes); @@ -169,7 +171,9 @@ fn test_kclique_rejects_malformed_or_non_clique_selections() { vec![true, true, false, false, true], vec![true, false, true, false, true], ] { - assert!(reduction.extract_solution(&bad).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &bad), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/ksatisfiability_kernel.rs b/src/unit_tests/rules/ksatisfiability_kernel.rs index 200045a36..902b13f78 100644 --- a/src/unit_tests/rules/ksatisfiability_kernel.rs +++ b/src/unit_tests/rules/ksatisfiability_kernel.rs @@ -174,7 +174,9 @@ fn test_ksatisfiability_to_kernel_rejects_non_kernel() { let source = KSatisfiability::::new(1, vec![CNFClause::new(vec![1, 1, 1])]); let reduction = ReduceTo::::reduce_to(&source).unwrap(); for config in [vec![], vec![false; 5], vec![true; 5], vec![false; 6]] { - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/ksatisfiability_monochromatictriangle.rs b/src/unit_tests/rules/ksatisfiability_monochromatictriangle.rs index eaa8484bd..e17ea015d 100644 --- a/src/unit_tests/rules/ksatisfiability_monochromatictriangle.rs +++ b/src/unit_tests/rules/ksatisfiability_monochromatictriangle.rs @@ -12,7 +12,7 @@ use crate::variant::K3; fn test_monochromatic_triangle_sender_all_colorings() { let mut edges = vec![(0, 1), (2, 3)]; add_equality_sender(&mut edges, (0, 1), (2, 3), 4); - let sender = MonochromaticTriangle::new(SimpleGraph::new(7, edges)); + let sender = MonochromaticTriangle::new(SimpleGraph::new(7, edges).unwrap()); assert_eq!(sender.num_edges(), 17); assert_eq!(sender.num_triangles(), 19); let mut extensions = [0, 0]; @@ -99,7 +99,9 @@ fn test_ksatisfiability_to_monochromatic_triangle_closed_loop() { .unwrap() .0 ); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); } #[test] diff --git a/src/unit_tests/rules/ksatisfiability_oneinthreesatisfiability.rs b/src/unit_tests/rules/ksatisfiability_oneinthreesatisfiability.rs index 5792a4456..69b2b5e8d 100644 --- a/src/unit_tests/rules/ksatisfiability_oneinthreesatisfiability.rs +++ b/src/unit_tests/rules/ksatisfiability_oneinthreesatisfiability.rs @@ -181,7 +181,9 @@ fn test_oneinthree_rejects_infeasible_target_assignments() { let source = KSatisfiability::::new(1, vec![CNFClause::new(vec![1; 3])]); let reduction = ReduceTo::::reduce_to(&source).unwrap(); for config in [vec![], vec![false; 9], vec![true; 9], vec![false; 10]] { - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs b/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs index 375509297..47bd055fc 100644 --- a/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs +++ b/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs @@ -30,9 +30,14 @@ fn solve_threshold_schedule_via_ilp( target.num_processors(), i64::try_from(deadline).unwrap(), target.precedences().to_vec(), - ); + ) + .unwrap(); let pcs_to_ilp = ReduceTo::>::reduce_to(&pcs).expect("reduction should succeed"); - let ilp_solution = ILPSolver::new().solve(pcs_to_ilp.target_problem()).ok()?; + let ilp_solution = match ILPSolver::new().solve(pcs_to_ilp.target_problem()) { + Ok(solution) => solution, + Err(crate::solvers::ILPSolveError::Infeasible) => return None, + Err(error) => panic!("ILP execution failed: {error}"), + }; let slot_assignment = pcs_to_ilp.extract_solution(&ilp_solution).unwrap(); let mut config = vec![vec![false; target.d_max()]; target.num_tasks()]; diff --git a/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs b/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs index 098a88b18..c1e7a6287 100644 --- a/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs +++ b/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs @@ -69,8 +69,6 @@ fn test_native_clauses_and_arbitrary_crt_signs() { ); } recovered.insert(extracted); - } else { - assert!(extracted.is_err()); } } // Enumerate only appearing variables; unused coordinates are free. @@ -177,7 +175,9 @@ fn test_rejects_infeasible_and_out_of_bound_integers() { reduction.target.c() + 1u32, ] { assert_eq!(reduction.target.evaluate(&witness).unwrap(), Or(false)); - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/ksatisfiability_qubo.rs b/src/unit_tests/rules/ksatisfiability_qubo.rs index 22aad0944..1b91ea920 100644 --- a/src/unit_tests/rules/ksatisfiability_qubo.rs +++ b/src/unit_tests/rules/ksatisfiability_qubo.rs @@ -102,7 +102,7 @@ fn test_ksatisfiability_to_qubo_structure() { let qubo = reduction.target_problem(); // QUBO should have at least the original variables - assert!(qubo.num_variables() >= ksat.num_vars()); + assert!(qubo.num_variables().unwrap() >= ksat.num_vars()); } #[test] @@ -124,7 +124,7 @@ fn test_k3satisfiability_to_qubo_closed_loop() { let qubo = reduction.target_problem(); // QUBO should have 5 + 7 = 12 variables - assert_eq!(qubo.num_variables(), 12); + assert_eq!(qubo.num_variables().unwrap(), 12); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -146,7 +146,7 @@ fn test_k3satisfiability_to_qubo_single_clause() { let qubo = reduction.target_problem(); // 3 vars + 1 auxiliary = 4 total - assert_eq!(qubo.num_variables(), 4); + assert_eq!(qubo.num_variables().unwrap(), 4); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -235,7 +235,7 @@ fn test_sat_qubo_all_short_clauses_and_raw_targets() { let decoded = reduction.extract_solution(&witness).unwrap(); assert!(source.evaluate(&decoded).unwrap().0); } else { - assert!(reduction.extract_solution(&witness).is_err()); + assert!(!matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() })); } minimum = minimum.min(energy); } @@ -250,10 +250,8 @@ fn test_sat_qubo_all_short_clauses_and_raw_targets() { AggregateReductionResult::extract_value(&reduction, Min(None)), Or(false) ); - assert!(reduction.extract_solution(&vec![]).is_err()); - assert!(reduction - .extract_solution(&vec![false; target.num_vars() + 1]) - .is_err()); + assert!(!matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() })); + assert!(!matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vars() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() })); } } for n in [0, 3] { @@ -272,29 +270,15 @@ fn test_sat_qubo_all_short_clauses_and_raw_targets() { #[test] fn test_sat_qubo_checked_numeric_boundaries() { - let mut matrix = vec![vec![i64::MAX]]; + let mut matrix = vec![std::collections::BTreeMap::from([(0, i64::MAX)])]; assert!(add_coefficient(&mut matrix, 0, 0, 1).is_err()); - let mut matrix = vec![vec![i64::MIN]]; + let mut matrix = vec![std::collections::BTreeMap::from([(0, i64::MIN)])]; assert!(add_coefficient(&mut matrix, 0, 0, -1).is_err()); assert!(build_qubo_matrix(usize::MAX, &[], 1).is_err()); - // This variable count is legal for the source on both 32- and 64-bit hosts, - // but its dense target cannot have an addressable number of entries. - let n = usize::MAX / 2; - let k2 = KSatisfiability::::new(n, vec![]); - let k3 = KSatisfiability::::new(n, vec![]); - assert!(matches!( - ReduceTo::>::reduce_to(&k2), - Err(crate::rules::ReductionError::IntegerOverflow { .. }) - )); - assert!(matches!( - ReduceTo::>::reduce_to(&k3), - Err(crate::rules::ReductionError::IntegerOverflow { .. }) - )); } #[test] fn test_sat_qubo_registered_aggregate_threshold() { - use crate::types::Or; macro_rules! check { ($k:ty) => { for (clauses, expected) in [(vec![vec![1]], true), (vec![vec![1], vec![-1]], false)] { @@ -315,14 +299,10 @@ fn test_sat_qubo_registered_aggregate_threshold() { && (e.target_variant_fn)() == QUBO::::variant() }) .unwrap(); - let aggregate = (edge.reduce_aggregate_fn.unwrap())(&source).unwrap(); + let step = (edge.reduce_fn.unwrap())(&source).unwrap(); assert_eq!( - *aggregate - .extract_value_from_solution_dyn(&witness) - .unwrap() - .downcast::() - .unwrap(), - Or(expected) + step.interpret_optimum.as_ref().unwrap()(&witness).unwrap(), + expected ); } }; diff --git a/src/unit_tests/rules/ksatisfiability_registersufficiency.rs b/src/unit_tests/rules/ksatisfiability_registersufficiency.rs index 301cf7af3..14c94748f 100644 --- a/src/unit_tests/rules/ksatisfiability_registersufficiency.rs +++ b/src/unit_tests/rules/ksatisfiability_registersufficiency.rs @@ -92,7 +92,9 @@ fn test_ksatisfiability_to_register_sufficiency_rejects_invalid_snapshot_order() let positions = positions_from_order(&order, target.num_vertices()); assert_eq!(target.evaluate(&positions).unwrap(), Or(false)); - assert!(reduction.extract_solution(&positions).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &positions), Ok(value) if { value.is_valid() }) + ); } #[test] @@ -163,7 +165,9 @@ fn test_ksatisfiability_to_registersufficiency_closed_loop_boundaries() { assert_eq!(extracted, vec![false; declared]); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); } else { - assert!(reduction.extract_solution(&vec![0]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0]), Ok(value) if { value.is_valid() }) + ); } } } @@ -217,7 +221,9 @@ fn test_short_repeated_and_tautological_clauses() { assert_eq!(decoded[i], original[i]); } } else { - assert!(reduction.extract_solution(&positions).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &positions), Ok(value) if { value.is_valid() }) + ); } } } diff --git a/src/unit_tests/rules/ksatisfiability_simultaneousincongruences.rs b/src/unit_tests/rules/ksatisfiability_simultaneousincongruences.rs index 6646e6d14..f36bb8157 100644 --- a/src/unit_tests/rules/ksatisfiability_simultaneousincongruences.rs +++ b/src/unit_tests/rules/ksatisfiability_simultaneousincongruences.rs @@ -17,7 +17,7 @@ fn test_ksatisfiability_to_simultaneous_incongruences_closed_loop() { .expect("reduction should succeed"); let target = reduction.target_problem(); - assert_eq!(target.lcm_moduli(), 15); + assert_eq!(target.lcm_moduli().unwrap(), 15); assert_eq!(target.num_pairs(), 6); let solver = BruteForce::new(); diff --git a/src/unit_tests/rules/ksatisfiability_timetabledesign.rs b/src/unit_tests/rules/ksatisfiability_timetabledesign.rs index e9c62c153..49d711323 100644 --- a/src/unit_tests/rules/ksatisfiability_timetabledesign.rs +++ b/src/unit_tests/rules/ksatisfiability_timetabledesign.rs @@ -118,10 +118,9 @@ fn test_ksatisfiability_to_timetabledesign_unsatisfiable() { let target_reduction = ReduceTo::>::reduce_to(reduction.target_problem()) .expect("timetable reduction should succeed"); - assert!( - ILPSolver::new() - .solve(target_reduction.target_problem()) - .is_err(), + assert_eq!( + ILPSolver::new().solve(target_reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "unsatisfiable 3SAT instance should produce an infeasible timetable" ); } diff --git a/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs b/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs index c9ca738b9..5a08a4254 100644 --- a/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs +++ b/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs @@ -11,11 +11,12 @@ use crate::types::Max; fn test_lengthboundeddisjointpaths_to_ilp_closed_loop() { // Diamond graph: 4 vertices, s=0, t=3, K=2 let source = LengthBoundedDisjointPaths::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]).unwrap(), 0, 3, 2, - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); assert_bf_vs_ilp(&source, &reduction); } @@ -23,11 +24,12 @@ fn test_lengthboundeddisjointpaths_to_ilp_closed_loop() { #[test] fn test_lengthboundeddisjointpaths_to_ilp_bf_vs_ilp() { let source = LengthBoundedDisjointPaths::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]).unwrap(), 0, 3, 2, - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } @@ -44,8 +46,9 @@ fn test_lengthboundeddisjointpaths_to_ilp_triangle_subgraphs() { .enumerate() .filter_map(|(i, &edge)| (mask & (1 << i) != 0).then_some(edge)) .collect(), - ); - let source = LengthBoundedDisjointPaths::new(graph, 0, 2, bound); + ) + .unwrap(); + let source = LengthBoundedDisjointPaths::new(graph, 0, 2, bound).unwrap(); let expected = i64::from(mask & 4 != 0) + i64::from(bound == 2 && mask & 3 == 3); let reference = BruteForce::new().solve(&source).unwrap().unwrap(); assert_eq!(source.evaluate(&reference).unwrap(), Max(Some(expected))); @@ -60,11 +63,12 @@ fn test_lengthboundeddisjointpaths_to_ilp_triangle_subgraphs() { #[test] fn test_lengthboundeddisjointpaths_to_ilp_preserves_edge_order() { let source = LengthBoundedDisjointPaths::new( - SimpleGraph::new(4, vec![(2, 0), (3, 1), (2, 1), (1, 0)]), + SimpleGraph::new(4, vec![(2, 0), (3, 1), (2, 1), (1, 0)]).unwrap(), 0, 2, 2, - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); // Reverse orientations of edge 0 (0->2) and edges 3,2 (0->1->2). let mut target_solution = vec![0; 18]; @@ -95,7 +99,8 @@ fn test_lengthboundeddisjointpaths_to_ilp_extracts_path_from_circulation() { ] { let mut edges = vec![(0, 1)]; edges.extend(cycle); - let source = LengthBoundedDisjointPaths::new(SimpleGraph::new(5, edges), 0, 1, 4); + let source = + LengthBoundedDisjointPaths::new(SimpleGraph::new(5, edges).unwrap(), 0, 1, 4).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target_solution = vec![1, 0, 1, 0, 1, 0, 1, 0, 1]; assert!(reduction @@ -110,9 +115,13 @@ fn test_lengthboundeddisjointpaths_to_ilp_extracts_path_from_circulation() { #[test] fn test_lengthboundeddisjointpaths_to_ilp_rejects_invalid_target_solutions() { - let source = LengthBoundedDisjointPaths::new(SimpleGraph::new(2, vec![(0, 1)]), 0, 1, 1); + let source = + LengthBoundedDisjointPaths::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 0, 1, 1) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); for solution in [vec![], vec![2, 0, 1], vec![0, 0, 1], vec![1, 0, 0]] { - assert!(reduction.extract_solution(&solution).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &solution), Ok(value) if value.is_valid()) + ); } } diff --git a/src/unit_tests/rules/longestcircuit_ilp.rs b/src/unit_tests/rules/longestcircuit_ilp.rs index 46f03d612..4cd09f678 100644 --- a/src/unit_tests/rules/longestcircuit_ilp.rs +++ b/src/unit_tests/rules/longestcircuit_ilp.rs @@ -8,9 +8,10 @@ use crate::traits::Problem; fn test_reduction_creates_valid_ilp() { // Triangle with unit lengths let problem = LongestCircuit::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1, 1, 1], - ); + ) + .unwrap(); let reduction: ReductionLongestCircuitToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -38,9 +39,11 @@ fn test_longestcircuit_to_ilp_closed_loop() { (2, 5), (3, 5), ], - ), + ) + .unwrap(), vec![3, 2, 4, 1, 5, 2, 3, 2, 1, 2], - ); + ) + .unwrap(); // BruteForce on source to verify feasibility let bf = BruteForce::new(); let bf_solution = bf @@ -71,9 +74,10 @@ fn test_longestcircuit_to_ilp_closed_loop() { fn test_longestcircuit_to_ilp_triangle() { // Triangle: all edges length 1 let problem = LongestCircuit::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1, 1, 1], - ); + ) + .unwrap(); let reduction: ReductionLongestCircuitToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -83,9 +87,10 @@ fn test_longestcircuit_to_ilp_triangle() { #[test] fn test_solution_extraction() { let problem = LongestCircuit::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0), (0, 2), (1, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0), (0, 2), (1, 3)]).unwrap(), vec![1, 1, 1, 1, 2, 2], - ); + ) + .unwrap(); let reduction: ReductionLongestCircuitToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); @@ -99,9 +104,10 @@ fn test_solution_extraction() { #[test] fn test_longestcircuit_to_ilp_bf_vs_ilp() { let problem = LongestCircuit::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1, 1, 1], - ); + ) + .unwrap(); let reduction: ReductionLongestCircuitToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); @@ -113,9 +119,10 @@ fn test_longestcircuit_to_ilp_cycle_excludes_any_vertex() { for leaf in 0..4 { let [a, b, c] = [1, 2, 3].map(|offset| (leaf + offset) % 4); let problem = LongestCircuit::new( - SimpleGraph::new(4, vec![(leaf, a), (a, b), (b, c), (c, a)]), + SimpleGraph::new(4, vec![(leaf, a), (a, b), (b, c), (c, a)]).unwrap(), vec![10, 1, 2, 3], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let target_solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); let extracted = reduction.extract_solution(&target_solution).unwrap(); @@ -133,7 +140,7 @@ fn test_longestcircuit_to_ilp_selects_one_best_cycle() { edges.push((2, 3)); lengths.push(20); } - let problem = LongestCircuit::new(SimpleGraph::new(6, edges), lengths); + let problem = LongestCircuit::new(SimpleGraph::new(6, edges).unwrap(), lengths).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let target_solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); let extracted = reduction.extract_solution(&target_solution).unwrap(); @@ -149,7 +156,7 @@ fn test_longestcircuit_to_ilp_acyclic_graphs() { for n in 0..4 { let edges: Vec<_> = (1..n).map(|v| (v - 1, v)).collect(); let m = edges.len(); - let problem = LongestCircuit::new(SimpleGraph::new(n, edges), vec![1; m]); + let problem = LongestCircuit::new(SimpleGraph::new(n, edges).unwrap(), vec![1; m]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let target = reduction.target_problem(); assert_eq!(target.num_vars(), m + 2 * n + 2 * m * n); diff --git a/src/unit_tests/rules/longestcommonsubsequence_ilp.rs b/src/unit_tests/rules/longestcommonsubsequence_ilp.rs index f920fa554..3c93411f0 100644 --- a/src/unit_tests/rules/longestcommonsubsequence_ilp.rs +++ b/src/unit_tests/rules/longestcommonsubsequence_ilp.rs @@ -6,7 +6,7 @@ use crate::types::Max; #[test] fn test_lcs_to_ilp_yes_instance() { - let problem = LongestCommonSubsequence::new(3, vec![vec![0, 1, 2], vec![1, 0, 2]]); + let problem = LongestCommonSubsequence::new(3, vec![vec![0, 1, 2], vec![1, 0, 2]]).unwrap(); let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -27,7 +27,8 @@ fn test_lcs_to_ilp_yes_instance() { #[test] fn test_lcs_to_ilp_closed_loop_three_strings() { let problem = - LongestCommonSubsequence::new(2, vec![vec![0, 1, 0], vec![1, 0, 1, 0], vec![0, 0, 1, 0]]); + LongestCommonSubsequence::new(2, vec![vec![0, 1, 0], vec![1, 0, 1, 0], vec![0, 0, 1, 0]]) + .unwrap(); let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -50,7 +51,7 @@ fn test_lcs_to_ilp_closed_loop_three_strings() { #[test] fn test_lcs_to_ilp_extracts_valid_witness() { - let problem = LongestCommonSubsequence::new(2, vec![vec![0, 1, 0], vec![1, 0, 1, 0]]); + let problem = LongestCommonSubsequence::new(2, vec![vec![0, 1, 0], vec![1, 0, 1, 0]]).unwrap(); let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -67,7 +68,7 @@ fn test_lcs_to_ilp_extracts_valid_witness() { #[test] fn test_lcs_to_ilp_matches_brute_force() { // Verify ILP optimal value matches brute force - let problem = LongestCommonSubsequence::new(2, vec![vec![0, 1, 0], vec![1, 0, 1]]); + let problem = LongestCommonSubsequence::new(2, vec![vec![0, 1, 0], vec![1, 0, 1]]).unwrap(); let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -88,7 +89,7 @@ fn test_lcs_to_ilp_matches_brute_force() { fn test_lcs_to_ilp_single_position_all_padding() { // When no common subsequence exists, the ILP should still find a solution // with all padding (length 0). - let problem = LongestCommonSubsequence::new(2, vec![vec![0, 0, 0], vec![1, 1, 1]]); + let problem = LongestCommonSubsequence::new(2, vec![vec![0, 0, 0], vec![1, 1, 1]]).unwrap(); let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -103,7 +104,7 @@ fn test_lcs_to_ilp_single_position_all_padding() { #[test] fn test_longestcommonsubsequence_to_ilp_bf_vs_ilp() { - let problem = LongestCommonSubsequence::new(2, vec![vec![0, 1, 0], vec![1, 0, 1]]); + let problem = LongestCommonSubsequence::new(2, vec![vec![0, 1, 0], vec![1, 0, 1]]).unwrap(); let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); diff --git a/src/unit_tests/rules/longestcommonsubsequence_maximumindependentset.rs b/src/unit_tests/rules/longestcommonsubsequence_maximumindependentset.rs index 7daf6a1e0..82d53f7bd 100644 --- a/src/unit_tests/rules/longestcommonsubsequence_maximumindependentset.rs +++ b/src/unit_tests/rules/longestcommonsubsequence_maximumindependentset.rs @@ -13,7 +13,8 @@ fn test_longestcommonsubsequence_to_maximumindependentset_closed_loop() { vec![0, 1, 0, 2], // ABAC vec![1, 0, 2, 0], // BACA ], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&lcs) .expect("reduction should succeed"); assert_optimization_round_trip_from_optimization_target( @@ -32,7 +33,8 @@ fn test_lcs_to_mis_graph_structure() { vec![0, 1, 0, 2], // ABAC vec![1, 0, 2, 0], // BACA ], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&lcs) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -46,14 +48,14 @@ fn test_lcs_to_mis_cross_frequency_product() { // s1="ABAC" has A:2, B:1, C:1 // s2="BACA" has A:2, B:1, C:1 // cross_freq = 2*2 + 1*1 + 1*1 = 6 - let lcs = LongestCommonSubsequence::new(3, vec![vec![0, 1, 0, 2], vec![1, 0, 2, 0]]); + let lcs = LongestCommonSubsequence::new(3, vec![vec![0, 1, 0, 2], vec![1, 0, 2, 0]]).unwrap(); assert_eq!(lcs.cross_frequency_product(), 6); } #[test] fn test_lcs_to_mis_optimal_value() { // LCS of "ABAC" and "BACA" is "BAC" (length 3) - let lcs = LongestCommonSubsequence::new(3, vec![vec![0, 1, 0, 2], vec![1, 0, 2, 0]]); + let lcs = LongestCommonSubsequence::new(3, vec![vec![0, 1, 0, 2], vec![1, 0, 2, 0]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&lcs) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -70,7 +72,8 @@ fn test_lcs_to_mis_optimal_value() { #[test] fn test_lcs_to_mis_three_strings() { // k=3 strings over binary alphabet - let lcs = LongestCommonSubsequence::new(2, vec![vec![0, 1, 0], vec![1, 0, 1], vec![0, 1, 1]]); + let lcs = LongestCommonSubsequence::new(2, vec![vec![0, 1, 0], vec![1, 0, 1], vec![0, 1, 1]]) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&lcs) .expect("reduction should succeed"); assert_optimization_round_trip_from_optimization_target( @@ -83,7 +86,7 @@ fn test_lcs_to_mis_three_strings() { #[test] fn test_lcs_to_mis_single_char_alphabet() { // All same character: LCS = min length - let lcs = LongestCommonSubsequence::new(1, vec![vec![0, 0, 0], vec![0, 0]]); + let lcs = LongestCommonSubsequence::new(1, vec![vec![0, 0, 0], vec![0, 0]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&lcs) .expect("reduction should succeed"); assert_optimization_round_trip_from_optimization_target( @@ -96,7 +99,7 @@ fn test_lcs_to_mis_single_char_alphabet() { #[test] fn test_lcs_to_mis_no_common_chars() { // No common characters: LCS = 0 - let lcs = LongestCommonSubsequence::new(2, vec![vec![0, 0, 0], vec![1, 1, 1]]); + let lcs = LongestCommonSubsequence::new(2, vec![vec![0, 0, 0], vec![1, 1, 1]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&lcs) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -115,7 +118,8 @@ fn test_lcs_to_mis_extract_solution() { vec![0, 1, 0, 2], // ABAC vec![1, 0, 2, 0], // BACA ], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&lcs) .expect("reduction should succeed"); @@ -139,7 +143,8 @@ fn test_lcs_to_mis_extract_solution() { fn test_lcs_to_mis_four_strings() { // k=4 strings let lcs = - LongestCommonSubsequence::new(2, vec![vec![0, 1], vec![1, 0], vec![0, 1], vec![1, 0]]); + LongestCommonSubsequence::new(2, vec![vec![0, 1], vec![1, 0], vec![0, 1], vec![1, 0]]) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&lcs) .expect("reduction should succeed"); assert_optimization_round_trip_from_optimization_target( diff --git a/src/unit_tests/rules/longestpath_ilp.rs b/src/unit_tests/rules/longestpath_ilp.rs index 8e23b37be..7ee27bb2a 100644 --- a/src/unit_tests/rules/longestpath_ilp.rs +++ b/src/unit_tests/rules/longestpath_ilp.rs @@ -21,15 +21,23 @@ fn issue_problem() -> LongestPath { (5, 6), (1, 6), ], - ), + ) + .unwrap(), vec![3, 2, 4, 1, 5, 2, 3, 2, 4, 1], 0, 6, ) + .unwrap() } fn simple_path_problem() -> LongestPath { - LongestPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![2, 3], 0, 2) + LongestPath::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![2, 3], + 0, + 2, + ) + .unwrap() } #[test] @@ -95,11 +103,12 @@ fn test_solution_extraction_from_handcrafted_ilp_assignment() { #[test] fn test_source_equals_target_uses_empty_path() { let problem = LongestPath::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![4, 5, 6], 1, 1, - ); + ) + .unwrap(); let reduction: ReductionLongestPathToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); diff --git a/src/unit_tests/rules/maxcut_minimumcutintoboundedsets.rs b/src/unit_tests/rules/maxcut_minimumcutintoboundedsets.rs index 51c788b17..3e9a9cb36 100644 --- a/src/unit_tests/rules/maxcut_minimumcutintoboundedsets.rs +++ b/src/unit_tests/rules/maxcut_minimumcutintoboundedsets.rs @@ -8,9 +8,10 @@ use crate::topology::SimpleGraph; fn test_maxcut_to_minimumcutintoboundedsets_closed_loop() { // Triangle K_3 with unit weights: max cut = 2 let source = MaxCut::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64, 1, 1], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); assert_optimization_round_trip_from_optimization_target( @@ -23,7 +24,7 @@ fn test_maxcut_to_minimumcutintoboundedsets_closed_loop() { #[test] fn test_maxcut_to_minimumcutintoboundedsets_single_edge() { // Single edge K_2: max cut = 1 - let source = MaxCut::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i64]); + let source = MaxCut::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![1i64]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); assert_optimization_round_trip_from_optimization_target( @@ -37,9 +38,10 @@ fn test_maxcut_to_minimumcutintoboundedsets_single_edge() { fn test_maxcut_to_minimumcutintoboundedsets_path_p4() { // Path P_4: vertices 0-1-2-3, unit weights, max cut = 3 (alternate: 0,1,0,1) let source = MaxCut::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64, 1, 1], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); assert_optimization_round_trip_from_optimization_target( @@ -53,9 +55,10 @@ fn test_maxcut_to_minimumcutintoboundedsets_path_p4() { fn test_maxcut_to_minimumcutintoboundedsets_weighted() { // Triangle with weights [1, 2, 3]: max cut = 5 (cut edges with weights 2 and 3) let source = MaxCut::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64, 2, 3], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); assert_optimization_round_trip_from_optimization_target( @@ -69,9 +72,10 @@ fn test_maxcut_to_minimumcutintoboundedsets_weighted() { fn test_maxcut_to_minimumcutintoboundedsets_target_structure() { // Verify the target problem structure for a 3-vertex graph let source = MaxCut::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64, 1, 1], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -91,9 +95,10 @@ fn test_maxcut_to_minimumcutintoboundedsets_target_structure() { fn test_maxcut_to_minimumcutintoboundedsets_even_vertices() { // Even number of vertices: n=4, n'=4, N=8 let source = MaxCut::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]).unwrap(), vec![1i64, 1, 1, 1], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -116,9 +121,10 @@ fn test_maxcut_to_minimumcutintoboundedsets_even_vertices() { fn test_maxcut_to_minimumcutintoboundedsets_extract_solution_size() { // Verify extract_solution returns only original vertices let source = MaxCut::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64, 1, 1], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); @@ -132,7 +138,7 @@ fn test_maxcut_to_minimumcutintoboundedsets_extract_solution_size() { fn test_maxcut_to_minimumcutintoboundedsets_weight_inversion() { // Verify weight inversion: original edge gets W_max - w, non-edge gets W_max // Use n=2 to keep the target small: n'=2, N=4, K_4 has 6 edges - let source = MaxCut::new(SimpleGraph::new(2, vec![(0, 1)]), vec![5i64]); + let source = MaxCut::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![5i64]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/unit_tests/rules/maxcut_minimummatrixcover.rs b/src/unit_tests/rules/maxcut_minimummatrixcover.rs index 7e6ef4a1b..b2acbd248 100644 --- a/src/unit_tests/rules/maxcut_minimummatrixcover.rs +++ b/src/unit_tests/rules/maxcut_minimummatrixcover.rs @@ -51,9 +51,10 @@ fn verify_identity(source: &MaxCut) { fn test_maxcut_to_minimummatrixcover_closed_loop_c4() { // C_4 with unit weights: max cut = 4 (partition {0,2} vs {1,3} cuts all edges). let source = MaxCut::::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]).unwrap(), vec![1, 1, 1, 1], - ); + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); assert_optimization_round_trip_from_optimization_target( @@ -86,8 +87,11 @@ fn test_maxcut_to_minimummatrixcover_closed_loop_c4() { #[test] fn test_maxcut_to_minimummatrixcover_closed_loop_p3_weighted() { // Path P_3 = 0-1-2 with weights (2, 3): max cut = 5 (split {1} vs {0, 2}). - let source = - MaxCut::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![2, 3]); + let source = MaxCut::::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![2, 3], + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); assert_optimization_round_trip_from_optimization_target( @@ -115,9 +119,10 @@ fn test_maxcut_to_minimummatrixcover_closed_loop_p3_weighted() { fn test_maxcut_to_minimummatrixcover_closed_loop_triangle() { // K_3 (triangle) with unit weights: max cut = 2. let source = MaxCut::::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1, 1, 1], - ); + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); assert_optimization_round_trip_from_optimization_target( @@ -141,9 +146,10 @@ fn test_maxcut_to_minimummatrixcover_closed_loop_triangle() { fn test_target_structure_matches_adjacency_matrix() { // Verify the construction details on an asymmetric weighted graph. let source = MaxCut::::new( - SimpleGraph::new(4, vec![(0, 1), (0, 3), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 3), (1, 2), (2, 3)]).unwrap(), vec![5, 7, 2, 3], - ); + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -173,16 +179,20 @@ fn test_target_structure_matches_adjacency_matrix() { fn test_algebraic_identity_c4_unit() { // The identity Σ a_ij f(i) f(j) = 2W − 4·cut(S) must hold for every f. let source = MaxCut::::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]).unwrap(), vec![1, 1, 1, 1], - ); + ) + .unwrap(); verify_identity(&source); } #[test] fn test_algebraic_identity_p3_weighted() { - let source = - MaxCut::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![2, 3]); + let source = MaxCut::::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![2, 3], + ) + .unwrap(); verify_identity(&source); } @@ -190,16 +200,20 @@ fn test_algebraic_identity_p3_weighted() { fn test_algebraic_identity_triangle_weighted() { // Triangle with non-uniform weights. let source = MaxCut::::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![4, 1, 2], - ); + ) + .unwrap(); verify_identity(&source); } #[test] fn test_extract_solution_is_identity() { - let source = - MaxCut::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 1]); + let source = MaxCut::::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1, 1], + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target_sol = vec![true, false, true]; @@ -209,7 +223,8 @@ fn test_extract_solution_is_identity() { #[test] fn test_empty_graph() { // n vertices, zero edges: matrix is all zeros, max cut = 0. - let source = MaxCut::::new(SimpleGraph::new(3, vec![]), vec![]); + let source = + MaxCut::::new(SimpleGraph::new(3, vec![]).unwrap(), vec![]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -233,7 +248,8 @@ fn test_overhead_num_rows_equals_num_vertices() { for n in [1usize, 2, 5, 8] { let edges: Vec<(usize, usize)> = (0..n.saturating_sub(1)).map(|i| (i, i + 1)).collect(); let weights: Vec = vec![1; edges.len()]; - let source = MaxCut::::new(SimpleGraph::new(n, edges), weights); + let source = + MaxCut::::new(SimpleGraph::new(n, edges).unwrap(), weights).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); assert_eq!(reduction.target_problem().num_rows(), n); @@ -243,7 +259,9 @@ fn test_overhead_num_rows_equals_num_vertices() { #[test] fn test_negative_weight_is_rejected() { // The reduction only handles nonnegative weights. - let source = MaxCut::::new(SimpleGraph::new(2, vec![(0, 1)]), vec![-1]); + let source = + MaxCut::::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![-1]) + .unwrap(); let error = ReduceTo::::reduce_to(&source).unwrap_err(); assert!(matches!( error, diff --git a/src/unit_tests/rules/maximalis_ilp.rs b/src/unit_tests/rules/maximalis_ilp.rs index 928156425..db798404e 100644 --- a/src/unit_tests/rules/maximalis_ilp.rs +++ b/src/unit_tests/rules/maximalis_ilp.rs @@ -6,7 +6,11 @@ use crate::traits::Problem; #[test] fn test_reduction_creates_valid_ilp() { // Path P3: 0-1-2 - let problem = MaximalIS::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 1, 1]); + let problem = MaximalIS::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1, 1, 1], + ) + .unwrap(); let reduction: ReductionMxISToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -18,9 +22,10 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_maximalis_to_ilp_bf_vs_ilp() { let problem = MaximalIS::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1, 1, 1, 1], - ); + ) + .unwrap(); let reduction: ReductionMxISToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -41,7 +46,11 @@ fn test_maximalis_to_ilp_bf_vs_ilp() { #[test] fn test_solution_extraction() { - let problem = MaximalIS::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 1, 1]); + let problem = MaximalIS::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1, 1, 1], + ) + .unwrap(); let reduction: ReductionMxISToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); @@ -55,7 +64,7 @@ fn test_solution_extraction() { #[test] fn test_maximalis_to_ilp_trivial() { // Single vertex - let problem = MaximalIS::new(SimpleGraph::new(1, vec![]), vec![1]); + let problem = MaximalIS::new(SimpleGraph::new(1, vec![]).unwrap(), vec![1]).unwrap(); let reduction: ReductionMxISToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/maximumclique_ilp.rs b/src/unit_tests/rules/maximumclique_ilp.rs index 4cc94c341..592850adb 100644 --- a/src/unit_tests/rules/maximumclique_ilp.rs +++ b/src/unit_tests/rules/maximumclique_ilp.rs @@ -53,9 +53,10 @@ fn test_reduction_creates_valid_ilp() { // Triangle graph: 3 vertices, 3 edges (complete graph K3) // All pairs are adjacent, so no constraints should be added let problem: MaximumClique = MaximumClique::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1; 3], - ); + ) + .unwrap(); let reduction: ReductionCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -73,8 +74,11 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_reduction_with_non_edges() { // Path graph 0-1-2: edges (0,1) and (1,2), non-edge (0,2) - let problem: MaximumClique = - MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1; 3]); + let problem: MaximumClique = MaximumClique::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1; 3], + ) + .unwrap(); let reduction: ReductionCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -91,7 +95,7 @@ fn test_reduction_with_non_edges() { #[test] fn test_reduction_weighted() { let problem: MaximumClique = - MaximumClique::new(SimpleGraph::new(3, vec![(0, 1)]), vec![5, 10, 15]); + MaximumClique::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![5, 10, 15]).unwrap(); let reduction: ReductionCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -108,9 +112,10 @@ fn test_reduction_weighted() { fn test_maximumclique_to_ilp_closed_loop() { // Triangle graph (K3): max clique = 3 vertices let problem: MaximumClique = MaximumClique::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1; 3], - ); + ) + .unwrap(); let reduction: ReductionCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -140,9 +145,10 @@ fn test_maximumclique_to_ilp_closed_loop() { fn test_ilp_solution_equals_brute_force_path() { // Path graph 0-1-2-3: max clique = 2 (any adjacent pair) let problem: MaximumClique = MaximumClique::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1; 4], - ); + ) + .unwrap(); let reduction: ReductionCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -170,8 +176,11 @@ fn test_ilp_solution_equals_brute_force_weighted() { // Weights: [1, 100, 1] // Max clique by weight: {0, 1} (weight 101) or {1, 2} (weight 101), or just {1} (weight 100) // Since 0-1 and 1-2 are edges, both {0,1} and {1,2} are valid cliques - let problem: MaximumClique = - MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 100, 1]); + let problem: MaximumClique = MaximumClique::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1, 100, 1], + ) + .unwrap(); let reduction: ReductionCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -193,8 +202,11 @@ fn test_ilp_solution_equals_brute_force_weighted() { #[test] fn test_solution_extraction() { - let problem: MaximumClique = - MaximumClique::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), vec![1; 4]); + let problem: MaximumClique = MaximumClique::new( + SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(), + vec![1; 4], + ) + .unwrap(); let reduction: ReductionCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -210,9 +222,10 @@ fn test_solution_extraction() { #[test] fn test_ilp_structure() { let problem: MaximumClique = MaximumClique::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(), vec![1; 5], - ); + ) + .unwrap(); let reduction: ReductionCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -226,7 +239,7 @@ fn test_ilp_structure() { fn test_empty_graph() { // Graph with no edges: max clique = 1 (any single vertex) let problem: MaximumClique = - MaximumClique::new(SimpleGraph::new(3, vec![]), vec![1; 3]); + MaximumClique::new(SimpleGraph::new(3, vec![]).unwrap(), vec![1; 3]).unwrap(); let reduction: ReductionCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -249,9 +262,10 @@ fn test_empty_graph() { fn test_complete_graph() { // Complete graph K4: max clique = 4 (all vertices) let problem: MaximumClique = MaximumClique::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), vec![1; 4], - ); + ) + .unwrap(); let reduction: ReductionCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -275,9 +289,10 @@ fn test_bipartite_graph() { // Bipartite graph: 0-2, 0-3, 1-2, 1-3 (two independent sets: {0,1} and {2,3}) // Max clique = 2 (any edge, e.g., {0, 2}) let problem: MaximumClique = MaximumClique::new( - SimpleGraph::new(4, vec![(0, 2), (0, 3), (1, 2), (1, 3)]), + SimpleGraph::new(4, vec![(0, 2), (0, 3), (1, 2), (1, 3)]).unwrap(), vec![1; 4], - ); + ) + .unwrap(); let reduction: ReductionCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -299,9 +314,10 @@ fn test_star_graph() { // Star graph: center 0 connected to 1, 2, 3 // Max clique = 2 (center + any leaf) let problem: MaximumClique = MaximumClique::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(), vec![1; 4], - ); + ) + .unwrap(); let reduction: ReductionCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -320,9 +336,10 @@ fn test_star_graph() { #[test] fn test_maximumclique_to_ilp_bf_vs_ilp() { let problem: MaximumClique = MaximumClique::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1; 4], - ); + ) + .unwrap(); let reduction: ReductionCliqueToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); diff --git a/src/unit_tests/rules/maximumclique_maximumindependentset.rs b/src/unit_tests/rules/maximumclique_maximumindependentset.rs index 2097108cf..6dfe22554 100644 --- a/src/unit_tests/rules/maximumclique_maximumindependentset.rs +++ b/src/unit_tests/rules/maximumclique_maximumindependentset.rs @@ -11,9 +11,10 @@ fn test_maximumclique_to_maximumindependentset_closed_loop() { // Maximum clique is any edge, size 2. // Complement has edges {(0,2),(0,3),(1,3)}, MIS of size 2. let source = MaximumClique::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -35,9 +36,10 @@ fn test_maximumclique_to_maximumindependentset_triangle() { // Complement is empty graph (no edges) // MIS on empty graph = all vertices let source = MaximumClique::new( - SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -60,7 +62,11 @@ fn test_maximumclique_to_maximumindependentset_triangle() { #[test] fn test_maximumclique_to_maximumindependentset_weights_preserved() { - let source = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![10, 20, 30]); + let source = MaximumClique::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![10, 20, 30], + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -72,7 +78,7 @@ fn test_maximumclique_to_maximumindependentset_weights_preserved() { fn test_maximumclique_to_maximumindependentset_empty_graph() { // Empty graph (no edges): complement is complete graph // Max clique in empty graph = any single vertex - let source = MaximumClique::new(SimpleGraph::new(3, vec![]), vec![1i64; 3]); + let source = MaximumClique::new(SimpleGraph::new(3, vec![]).unwrap(), vec![1i64; 3]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -94,9 +100,10 @@ fn test_maximumclique_to_maximumindependentset_one_weights_closed_loop() { // Same P4 as the i64 closed-loop test, but with unit weights so the // reduction stays on the endpoint (no i64 detour). let source = MaximumClique::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![One; 4], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -115,9 +122,10 @@ fn test_maximumclique_to_maximumindependentset_one_weights_closed_loop() { fn test_maximumclique_to_maximumindependentset_overhead() { // Verify exact size formula: complement edges = n*(n-1)/2 - m let source = MaximumClique::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(), vec![1i64; 5], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/unit_tests/rules/maximumcokplex_ilp.rs b/src/unit_tests/rules/maximumcokplex_ilp.rs index aa92d32f2..e8c943f9c 100644 --- a/src/unit_tests/rules/maximumcokplex_ilp.rs +++ b/src/unit_tests/rules/maximumcokplex_ilp.rs @@ -9,11 +9,11 @@ use crate::types::{Max, One}; use crate::variant::KN; fn c5() -> SimpleGraph { - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]) + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap() } fn issue_instance() -> MaximumCoKPlex { - MaximumCoKPlex::<_, i64, KN>::with_k(c5(), vec![5, 1, 4, 1, 3], 2) + MaximumCoKPlex::<_, i64, KN>::with_k(c5(), vec![5, 1, 4, 1, 3], 2).unwrap() } #[test] @@ -64,7 +64,7 @@ fn test_maximumcokplex_to_ilp_bf_vs_ilp() { #[test] fn test_maximumcokplex_to_ilp_k_equals_1_regression() { - let source = MaximumCoKPlex::<_, One, KN>::with_k(c5(), vec![One; 5], 1); + let source = MaximumCoKPlex::<_, One, KN>::with_k(c5(), vec![One; 5], 1).unwrap(); let reduction: ReductionCoKPlexToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() diff --git a/src/unit_tests/rules/maximumcommonedgesubgraph_ilp.rs b/src/unit_tests/rules/maximumcommonedgesubgraph_ilp.rs index 7d63945d2..5489336b1 100644 --- a/src/unit_tests/rules/maximumcommonedgesubgraph_ilp.rs +++ b/src/unit_tests/rules/maximumcommonedgesubgraph_ilp.rs @@ -13,11 +13,13 @@ fn matched_paths() -> MaximumCommonEdgeSubgraph { LabelledDigraph::new( 3, vec![LabelledArc::new(0, 0, 1), LabelledArc::new(1, 1, 2)], - ), + ) + .unwrap(), LabelledDigraph::new( 3, vec![LabelledArc::new(0, 0, 1), LabelledArc::new(1, 1, 2)], - ), + ) + .unwrap(), ) } @@ -31,8 +33,9 @@ fn truncated_instance() -> MaximumCommonEdgeSubgraph { LabelledArc::new(0, 0, 1), LabelledArc::new(1, 7, 2), // label 7 absent in G2 ], - ), - LabelledDigraph::new(2, vec![LabelledArc::new(0, 0, 1)]), + ) + .unwrap(), + LabelledDigraph::new(2, vec![LabelledArc::new(0, 0, 1)]).unwrap(), ) } @@ -106,8 +109,8 @@ fn test_maximumcommonedgesubgraph_to_ilp_empty_graphs() { // Edge corner case: both graphs have no arcs. Optimum is 0 and the // resulting ILP has no y-variables and no McCormick constraints. let source = MaximumCommonEdgeSubgraph::new( - LabelledDigraph::new(2, vec![]), - LabelledDigraph::new(2, vec![]), + LabelledDigraph::new(2, vec![]).unwrap(), + LabelledDigraph::new(2, vec![]).unwrap(), ); let reduction: ReductionMCESToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); @@ -130,8 +133,8 @@ fn test_maximumcommonedgesubgraph_to_ilp_self_loop() { // Corner case: self-loops with matching labels. A single self-loop // mapped to a matching target self-loop preserves the arc. let source = MaximumCommonEdgeSubgraph::new( - LabelledDigraph::new(1, vec![LabelledArc::new(0, 3, 0)]), - LabelledDigraph::new(2, vec![LabelledArc::new(1, 3, 1)]), + LabelledDigraph::new(1, vec![LabelledArc::new(0, 3, 0)]).unwrap(), + LabelledDigraph::new(2, vec![LabelledArc::new(1, 3, 1)]).unwrap(), ); let reduction: ReductionMCESToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); diff --git a/src/unit_tests/rules/maximumdomaticnumber_ilp.rs b/src/unit_tests/rules/maximumdomaticnumber_ilp.rs index 54a6e2e1a..90f95dd54 100644 --- a/src/unit_tests/rules/maximumdomaticnumber_ilp.rs +++ b/src/unit_tests/rules/maximumdomaticnumber_ilp.rs @@ -7,7 +7,7 @@ use crate::types::Max; #[test] fn test_maximumdomaticnumber_to_ilp_closed_loop() { // Path P3: 0-1-2, domatic number = 2 - let problem = MaximumDomaticNumber::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = MaximumDomaticNumber::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction: ReductionDomaticNumberToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -35,7 +35,7 @@ fn test_maximumdomaticnumber_to_ilp_closed_loop() { #[test] fn test_maximumdomaticnumber_to_ilp_structure() { // P3: 3 vertices - let problem = MaximumDomaticNumber::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = MaximumDomaticNumber::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction: ReductionDomaticNumberToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -60,7 +60,7 @@ fn test_maximumdomaticnumber_to_ilp_structure() { #[test] fn test_maximumdomaticnumber_to_ilp_bf_vs_ilp() { // P3: 3 vertices, domatic number = 2 - let problem = MaximumDomaticNumber::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = MaximumDomaticNumber::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction: ReductionDomaticNumberToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); @@ -69,7 +69,8 @@ fn test_maximumdomaticnumber_to_ilp_bf_vs_ilp() { #[test] fn test_maximumdomaticnumber_to_ilp_complete_graph() { // K3: domatic number = 3 (each vertex is its own dominating set) - let problem = MaximumDomaticNumber::new(SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)])); + let problem = + MaximumDomaticNumber::new(SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap()); let reduction: ReductionDomaticNumberToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -85,7 +86,7 @@ fn test_maximumdomaticnumber_to_ilp_complete_graph() { #[test] fn test_maximumdomaticnumber_to_ilp_single_vertex() { // Single vertex: domatic number = 1 - let problem = MaximumDomaticNumber::new(SimpleGraph::new(1, vec![])); + let problem = MaximumDomaticNumber::new(SimpleGraph::new(1, vec![]).unwrap()); let reduction: ReductionDomaticNumberToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -101,7 +102,7 @@ fn test_maximumdomaticnumber_to_ilp_single_vertex() { #[test] fn test_maximumdomaticnumber_to_ilp_solution_extraction() { // P3: 0-1-2 - let problem = MaximumDomaticNumber::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = MaximumDomaticNumber::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction: ReductionDomaticNumberToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); diff --git a/src/unit_tests/rules/maximumedgeweightedkclique_ilp.rs b/src/unit_tests/rules/maximumedgeweightedkclique_ilp.rs index aaf58b7ee..5f867a78f 100644 --- a/src/unit_tests/rules/maximumedgeweightedkclique_ilp.rs +++ b/src/unit_tests/rules/maximumedgeweightedkclique_ilp.rs @@ -10,7 +10,7 @@ fn issue_instance() -> MaximumEdgeWeightedKClique { // 4 vertices, edges (0,1),(0,2),(1,2),(0,3),(1,3) with weights [5,4,-1,1,0], k=3. // Optimum induced weight is 5 + 4 + (-1) = 8 on clique {0, 1, 2}. MaximumEdgeWeightedKClique::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]).unwrap(), vec![5, 4, -1, 1, 0], 3, ) @@ -61,7 +61,7 @@ fn test_maximumedgeweightedkclique_to_ilp_negative_weight_excluded_via_extra_con // y >= x_u + x_v - 1 ensures negative-weight y's are forced to 1 when // both endpoints are selected. let source = MaximumEdgeWeightedKClique::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![-1, -1, -1], 3, ) diff --git a/src/unit_tests/rules/maximumindependentset_gridgraph.rs b/src/unit_tests/rules/maximumindependentset_gridgraph.rs index 7053dde31..ec414238a 100644 --- a/src/unit_tests/rules/maximumindependentset_gridgraph.rs +++ b/src/unit_tests/rules/maximumindependentset_gridgraph.rs @@ -56,7 +56,8 @@ fn test_mis_simple_one_to_kings_one_is_deterministic_on_large_graph() { } } - let problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges), vec![One; n]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(n, edges).unwrap(), vec![One; n]).unwrap(); let first = ReduceTo::>::reduce_to(&problem) .expect("reduction should succeed"); @@ -78,9 +79,10 @@ fn test_mis_simple_one_to_kings_one_is_deterministic_on_large_graph() { fn test_mis_simple_one_to_kings_one_closed_loop() { // Path graph: 0-1-2-3-4 (MIS = 3: select vertices 0, 2, 4) let problem = MaximumIndependentSet::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(), vec![One; 5], - ); + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&problem) .expect("reduction should succeed"); let target = result.target_problem(); @@ -113,7 +115,9 @@ fn test_mis_simple_one_to_kings_one_all_four_vertex_graphs() { .filter(|(index, _)| mask & (1 << index) != 0) .map(|(_, &edge)| edge) .collect::>(); - let source = MaximumIndependentSet::new(SimpleGraph::new(4, edges.clone()), vec![One; 4]); + let source = + MaximumIndependentSet::new(SimpleGraph::new(4, edges.clone()).unwrap(), vec![One; 4]) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); diff --git a/src/unit_tests/rules/maximumindependentset_ilp.rs b/src/unit_tests/rules/maximumindependentset_ilp.rs index b20f2b265..0c75fe17c 100644 --- a/src/unit_tests/rules/maximumindependentset_ilp.rs +++ b/src/unit_tests/rules/maximumindependentset_ilp.rs @@ -27,9 +27,10 @@ fn reduce_mis_to_ilp( #[test] fn test_maximumindependentset_to_ilp_via_path_structure() { let problem = MaximumIndependentSet::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); let (path, chain) = reduce_mis_to_ilp(&problem); let ilp: &ILP = chain.target_problem(); @@ -52,9 +53,10 @@ fn test_maximumindependentset_to_ilp_via_path_structure() { #[test] fn test_maximumindependentset_to_ilp_via_path_closed_loop() { let problem = MaximumIndependentSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let (_, chain) = reduce_mis_to_ilp(&problem); let ilp: &ILP = chain.target_problem(); @@ -69,8 +71,11 @@ fn test_maximumindependentset_to_ilp_via_path_closed_loop() { #[test] fn test_maximumindependentset_to_ilp_via_path_weighted() { - let problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 100, 1]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1, 100, 1], + ) + .unwrap(); let (_, chain) = reduce_mis_to_ilp(&problem); let ilp: &ILP = chain.target_problem(); @@ -85,9 +90,10 @@ fn test_maximumindependentset_to_ilp_via_path_weighted() { #[test] fn test_maximumindependentset_to_ilp_bf_vs_ilp() { let problem = MaximumIndependentSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let (_, chain) = reduce_mis_to_ilp(&problem); let ilp: &ILP = chain.target_problem(); let bf_value_solution = BruteForce::new().solve(&problem).unwrap().unwrap(); diff --git a/src/unit_tests/rules/maximumindependentset_maximumclique.rs b/src/unit_tests/rules/maximumindependentset_maximumclique.rs index f6e2813b1..4f76e92d1 100644 --- a/src/unit_tests/rules/maximumindependentset_maximumclique.rs +++ b/src/unit_tests/rules/maximumindependentset_maximumclique.rs @@ -8,9 +8,10 @@ use crate::types::One; fn test_maximumindependentset_to_maximumclique_closed_loop() { // Path graph: 0-1-2-3-4 let source = MaximumIndependentSet::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(), vec![1i64; 5], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -30,9 +31,10 @@ fn test_maximumindependentset_to_maximumclique_closed_loop() { fn test_maximumindependentset_to_maximumclique_weighted() { // Triangle with weights let source = MaximumIndependentSet::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![10, 20, 30], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -55,7 +57,8 @@ fn test_maximumindependentset_to_maximumclique_weighted() { #[test] fn test_maximumindependentset_to_maximumclique_empty_graph() { // Empty graph (no edges) - complement is complete graph - let source = MaximumIndependentSet::new(SimpleGraph::new(4, vec![]), vec![1i64; 4]); + let source = + MaximumIndependentSet::new(SimpleGraph::new(4, vec![]).unwrap(), vec![1i64; 4]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -76,9 +79,10 @@ fn test_maximumindependentset_to_maximumclique_empty_graph() { fn test_maximumindependentset_to_maximumclique_one_weights_closed_loop() { // Unit-weight closed loop: endpoint stays on One all the way. let source = MaximumIndependentSet::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(), vec![One; 5], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -97,9 +101,10 @@ fn test_maximumindependentset_to_maximumclique_one_weights_closed_loop() { fn test_maximumindependentset_to_maximumclique_complete_graph() { // Complete graph K4 - complement is empty graph let source = MaximumIndependentSet::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs b/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs index 202b4819b..b3600c7c6 100644 --- a/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs +++ b/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs @@ -1,13 +1,16 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; use crate::types::One; -include!("../jl_helpers.rs"); #[test] fn test_maximumindependentset_to_maximumsetpacking_closed_loop() { - let is_problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![10, 20, 30]); + let is_problem = MaximumIndependentSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![10, 20, 30], + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&is_problem) .expect("reduction should succeed"); let sp_problem = reduction.target_problem(); @@ -19,7 +22,8 @@ fn test_maximumindependentset_to_maximumsetpacking_closed_loop() { #[test] fn test_empty_graph() { // No edges means all sets are empty (or we need to handle it) - let is_problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![]), vec![1i64; 3]); + let is_problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![]).unwrap(), vec![1i64; 3]).unwrap(); let reduction = ReduceTo::>::reduce_to(&is_problem) .expect("reduction should succeed"); let sp_problem = reduction.target_problem(); @@ -51,8 +55,11 @@ fn test_disjoint_sets() { #[test] fn test_reduction_structure() { // Test IS to SP structure - let is_problem = - MaximumIndependentSet::new(SimpleGraph::new(4, vec![(0, 1), (1, 2)]), vec![1i64; 4]); + let is_problem = MaximumIndependentSet::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 4], + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&is_problem) .expect("reduction should succeed"); let sp = reduction.target_problem(); @@ -82,8 +89,11 @@ fn test_jl_parity_is_to_setpacking() { serde_json::from_str(include_str!("../../../tests/data/jl/independentset.json")).unwrap(); let inst = &is_data["instances"][0]["instance"]; let nv = inst["num_vertices"].as_u64().unwrap() as usize; - let source = - MaximumIndependentSet::new(SimpleGraph::new(nv, jl_parse_edges(inst)), vec![1i64; nv]); + let source = MaximumIndependentSet::new( + SimpleGraph::new(nv, jl_parse_edges(inst)).unwrap(), + vec![1i64; nv], + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let solver = BruteForce::new(); @@ -140,8 +150,11 @@ fn test_jl_parity_rule_is_to_setpacking() { serde_json::from_str(include_str!("../../../tests/data/jl/independentset.json")).unwrap(); let inst = &jl_find_instance_by_label(&is_data, "doc_4vertex")["instance"]; let nv = inst["num_vertices"].as_u64().unwrap() as usize; - let source = - MaximumIndependentSet::new(SimpleGraph::new(nv, jl_parse_edges(inst)), vec![1i64; nv]); + let source = MaximumIndependentSet::new( + SimpleGraph::new(nv, jl_parse_edges(inst)).unwrap(), + vec![1i64; nv], + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let solver = BruteForce::new(); @@ -171,8 +184,11 @@ fn test_jl_parity_doc_is_to_setpacking() { let is_instance = jl_find_instance_by_label(&is_data, "doc_4vertex"); let inst = &is_instance["instance"]; let nv = inst["num_vertices"].as_u64().unwrap() as usize; - let source = - MaximumIndependentSet::new(SimpleGraph::new(nv, jl_parse_edges(inst)), vec![1i64; nv]); + let source = MaximumIndependentSet::new( + SimpleGraph::new(nv, jl_parse_edges(inst)).unwrap(), + vec![1i64; nv], + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let solver = BruteForce::new(); @@ -194,8 +210,11 @@ fn test_jl_parity_doc_is_to_setpacking() { #[test] fn test_maximumindependentset_one_to_maximumsetpacking_closed_loop() { // Path graph: 0-1-2 with unit weights (MIS = 2: select vertices 0, 2) - let is_problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![One; 3]); + let is_problem = MaximumIndependentSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![One; 3], + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&is_problem) .expect("reduction should succeed"); let sp_problem = reduction.target_problem(); diff --git a/src/unit_tests/rules/maximumindependentset_qubo.rs b/src/unit_tests/rules/maximumindependentset_qubo.rs index 99db973b3..b07bca284 100644 --- a/src/unit_tests/rules/maximumindependentset_qubo.rs +++ b/src/unit_tests/rules/maximumindependentset_qubo.rs @@ -28,9 +28,10 @@ fn reduce_mis_to_qubo( #[test] fn test_maximumindependentset_to_qubo_via_path_closed_loop() { let problem = MaximumIndependentSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let (path, chain) = reduce_mis_to_qubo(&problem); let qubo: &QUBO = chain.target_problem(); @@ -42,7 +43,7 @@ fn test_maximumindependentset_to_qubo_via_path_closed_loop() { path.type_names(), vec!["MaximumIndependentSet", "MaximumSetPacking", "QUBO"] ); - assert_eq!(qubo.num_variables(), 4); + assert_eq!(qubo.num_variables().unwrap(), 4); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -55,8 +56,11 @@ fn test_maximumindependentset_to_qubo_via_path_closed_loop() { #[test] fn test_maximumindependentset_to_qubo_via_path_weighted() { - let problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 100, 1]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1, 100, 1], + ) + .unwrap(); let (_, chain) = reduce_mis_to_qubo(&problem); let qubo: &QUBO = chain.target_problem(); @@ -73,11 +77,12 @@ fn test_maximumindependentset_to_qubo_via_path_weighted() { #[test] fn test_maximumindependentset_to_qubo_via_path_empty_graph() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![]), vec![1i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![]).unwrap(), vec![1i64; 3]).unwrap(); let (_, chain) = reduce_mis_to_qubo(&problem); let qubo: &QUBO = chain.target_problem(); - assert_eq!(qubo.num_variables(), 3); + assert_eq!(qubo.num_variables().unwrap(), 3); let solver = BruteForce::new(); let qubo_solution = solver diff --git a/src/unit_tests/rules/maximumindependentset_triangular.rs b/src/unit_tests/rules/maximumindependentset_triangular.rs index 7205ac3f5..1ba7febba 100644 --- a/src/unit_tests/rules/maximumindependentset_triangular.rs +++ b/src/unit_tests/rules/maximumindependentset_triangular.rs @@ -26,7 +26,8 @@ fn test_mis_simple_one_to_triangular_is_deterministic_on_large_graph() { } } - let problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges), vec![One; n]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(n, edges).unwrap(), vec![One; n]).unwrap(); let first = ReduceTo::>::reduce_to(&problem) .expect("reduction should succeed"); let baseline_atoms = first.target_problem().graph().num_vertices(); @@ -46,8 +47,11 @@ fn test_mis_simple_one_to_triangular_is_deterministic_on_large_graph() { #[test] fn test_mis_simple_one_to_triangular_closed_loop() { // Path graph: 0-1-2 - let problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![One; 3]); + let problem = MaximumIndependentSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![One; 3], + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&problem) .expect("reduction should succeed"); let target = result.target_problem(); @@ -68,7 +72,9 @@ fn test_mis_simple_one_to_triangular_preserves_optimum_and_witness() { }; let edges = vec![(0, 1), (1, 2), (2, 3)]; - let source = MaximumIndependentSet::new(SimpleGraph::new(4, edges.clone()), vec![One; 4]); + let source = + MaximumIndependentSet::new(SimpleGraph::new(4, edges.clone()).unwrap(), vec![One; 4]) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); @@ -104,7 +110,9 @@ fn test_mis_simple_one_to_triangular_all_four_vertex_graphs() { .filter(|(index, _)| mask & (1 << index) != 0) .map(|(_, &edge)| edge) .collect::>(); - let source = MaximumIndependentSet::new(SimpleGraph::new(4, edges.clone()), vec![One; 4]); + let source = + MaximumIndependentSet::new(SimpleGraph::new(4, edges.clone()).unwrap(), vec![One; 4]) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); @@ -131,7 +139,9 @@ fn test_mis_simple_one_to_triangular_all_four_vertex_graphs() { #[test] fn test_mis_simple_one_to_triangular_graph_methods() { // Single edge graph: 0-1 - let problem = MaximumIndependentSet::new(SimpleGraph::new(2, vec![(0, 1)]), vec![One; 2]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![One; 2]) + .unwrap(); let result = ReduceTo::>::reduce_to(&problem) .expect("reduction should succeed"); let target = result.target_problem(); diff --git a/src/unit_tests/rules/maximumleafspanningtree_ilp.rs b/src/unit_tests/rules/maximumleafspanningtree_ilp.rs index be3068b63..741293356 100644 --- a/src/unit_tests/rules/maximumleafspanningtree_ilp.rs +++ b/src/unit_tests/rules/maximumleafspanningtree_ilp.rs @@ -10,25 +10,30 @@ use crate::types::Max; /// Small instance: 4 vertices, 4 edges (P4 with a shortcut). /// Vertices 0-1-2-3 plus edge 0-2. fn small_instance() -> MaximumLeafSpanningTree { - MaximumLeafSpanningTree::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 2)])) + MaximumLeafSpanningTree::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 2)]).unwrap()) + .unwrap() } /// Issue #897 canonical instance: 6 vertices, 9 edges. fn canonical_instance() -> MaximumLeafSpanningTree { - MaximumLeafSpanningTree::new(SimpleGraph::new( - 6, - vec![ - (0, 1), - (0, 2), - (0, 3), - (1, 4), - (2, 4), - (2, 5), - (3, 5), - (4, 5), - (1, 3), - ], - )) + MaximumLeafSpanningTree::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (0, 3), + (1, 4), + (2, 4), + (2, 5), + (3, 5), + (4, 5), + (1, 3), + ], + ) + .unwrap(), + ) + .unwrap() } #[test] @@ -94,6 +99,13 @@ fn test_solution_extraction_reads_edge_selector_prefix() { target_solution[0] = 1; // edge (0,1) target_solution[1] = 1; // edge (1,2) target_solution[2] = 1; // edge (2,3) + target_solution[4] = 1; // vertex 0 is a leaf + target_solution[7] = 1; // vertex 3 is a leaf + + // Root 0 supplies one unit to each other vertex along the path. + target_solution[8] = 3; // 0 -> 1 + target_solution[10] = 2; // 1 -> 2 + target_solution[12] = 1; // 2 -> 3 assert_eq!( reduction.extract_solution(&target_solution).unwrap(), @@ -125,7 +137,9 @@ fn test_maximumleafspanningtree_to_ilp_bf_vs_ilp() { #[test] fn test_maximumleafspanningtree_to_ilp_path_graph() { // Path P4: 0-1-2-3, only spanning tree is the path itself => 2 leaves - let problem = MaximumLeafSpanningTree::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = + MaximumLeafSpanningTree::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()) + .unwrap(); let reduction: ReductionMaximumLeafSpanningTreeToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -138,7 +152,9 @@ fn test_maximumleafspanningtree_to_ilp_path_graph() { #[test] fn test_maximumleafspanningtree_to_ilp_star_graph() { // Star K1,3: center 0, leaves 1,2,3 => 3 leaves - let problem = MaximumLeafSpanningTree::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)])); + let problem = + MaximumLeafSpanningTree::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap()) + .unwrap(); let reduction: ReductionMaximumLeafSpanningTreeToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -152,10 +168,10 @@ fn test_maximumleafspanningtree_to_ilp_star_graph() { #[test] fn test_maximumleafspanningtree_to_ilp_complete_graph() { // K4: 4 vertices, 6 edges. Star spanning tree has 3 leaves. - let problem = MaximumLeafSpanningTree::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )); + let problem = MaximumLeafSpanningTree::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), + ) + .unwrap(); let bf = BruteForce::new(); let bf_solutions = bf.find_all_witnesses(&problem).unwrap(); let bf_value = problem.evaluate(&bf_solutions[0]).unwrap(); diff --git a/src/unit_tests/rules/maximumlikelihoodranking_ilp.rs b/src/unit_tests/rules/maximumlikelihoodranking_ilp.rs index e706c0c43..01be83265 100644 --- a/src/unit_tests/rules/maximumlikelihoodranking_ilp.rs +++ b/src/unit_tests/rules/maximumlikelihoodranking_ilp.rs @@ -8,7 +8,7 @@ use crate::types::Min; #[test] fn test_maximumlikelihoodranking_to_ilp_closed_loop() { let matrix = vec![vec![0, 3, 2], vec![2, 0, 4], vec![3, 1, 0]]; - let problem = MaximumLikelihoodRanking::new(matrix); + let problem = MaximumLikelihoodRanking::new(matrix).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_bf_vs_ilp(&problem, &reduction); @@ -17,7 +17,7 @@ fn test_maximumlikelihoodranking_to_ilp_closed_loop() { #[test] fn test_maximumlikelihoodranking_to_ilp_structure() { let matrix = vec![vec![0, 3, 2], vec![2, 0, 4], vec![3, 1, 0]]; - let problem = MaximumLikelihoodRanking::new(matrix); + let problem = MaximumLikelihoodRanking::new(matrix).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -36,7 +36,7 @@ fn test_maximumlikelihoodranking_to_ilp_bf_vs_ilp() { vec![2, 1, 0, 4], vec![0, 2, 1, 0], ]; - let problem = MaximumLikelihoodRanking::new(matrix); + let problem = MaximumLikelihoodRanking::new(matrix).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf_solutions = BruteForce::new().find_all_witnesses(&problem).unwrap(); @@ -56,7 +56,7 @@ fn test_maximumlikelihoodranking_to_ilp_bf_vs_ilp() { fn test_maximumlikelihoodranking_to_ilp_extraction() { // 3 items: simple instance let matrix = vec![vec![0, 3, 2], vec![2, 0, 4], vec![3, 1, 0]]; - let problem = MaximumLikelihoodRanking::new(matrix); + let problem = MaximumLikelihoodRanking::new(matrix).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() @@ -79,7 +79,7 @@ fn test_maximumlikelihoodranking_to_ilp_extraction() { #[test] fn test_maximumlikelihoodranking_to_ilp_two_items() { let matrix = vec![vec![0, 5], vec![3, 0]]; - let problem = MaximumLikelihoodRanking::new(matrix); + let problem = MaximumLikelihoodRanking::new(matrix).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -100,7 +100,7 @@ fn test_maximumlikelihoodranking_to_ilp_two_items() { #[test] fn test_maximumlikelihoodranking_to_ilp_single_item() { - let problem = MaximumLikelihoodRanking::new(vec![vec![0]]); + let problem = MaximumLikelihoodRanking::new(vec![vec![0]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -123,7 +123,7 @@ fn test_maximumlikelihoodranking_to_ilp_larger_instance() { vec![2, 1, 0, 4], vec![0, 2, 1, 0], ]; - let problem = MaximumLikelihoodRanking::new(matrix); + let problem = MaximumLikelihoodRanking::new(matrix).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/maximummatching_ilp.rs b/src/unit_tests/rules/maximummatching_ilp.rs index 3dda34bfe..12bd024b3 100644 --- a/src/unit_tests/rules/maximummatching_ilp.rs +++ b/src/unit_tests/rules/maximummatching_ilp.rs @@ -7,8 +7,9 @@ use crate::types::Max; #[test] fn test_reduction_creates_valid_ilp() { // Triangle graph: 3 vertices, 3 edges - let problem = - MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let problem = MaximumMatching::<_, i64>::unit_weights( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), + ); let reduction: ReductionMatchingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -31,7 +32,11 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_reduction_weighted() { - let problem = MaximumMatching::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![5, 10]); + let problem = MaximumMatching::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![5, 10], + ) + .unwrap(); let reduction: ReductionMatchingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -47,8 +52,9 @@ fn test_reduction_weighted() { #[test] fn test_maximummatching_to_ilp_closed_loop() { // Triangle graph: max matching = 1 edge - let problem = - MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let problem = MaximumMatching::<_, i64>::unit_weights( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), + ); let reduction: ReductionMatchingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -79,8 +85,9 @@ fn test_maximummatching_to_ilp_closed_loop() { #[test] fn test_ilp_solution_equals_brute_force_path() { // Path graph 0-1-2-3: max matching = 2 (edges {0-1, 2-3}) - let problem = - MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = MaximumMatching::<_, i64>::unit_weights( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), + ); let reduction: ReductionMatchingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -110,7 +117,11 @@ fn test_ilp_solution_equals_brute_force_weighted() { // 0 -- 1 -- 2 // Weights: [100, 1] // Max matching by weight: just edge 0-1 (weight 100) beats edge 1-2 (weight 1) - let problem = MaximumMatching::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![100, 1]); + let problem = MaximumMatching::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![100, 1], + ) + .unwrap(); let reduction: ReductionMatchingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -135,7 +146,7 @@ fn test_ilp_solution_equals_brute_force_weighted() { #[test] fn test_solution_extraction() { let problem = - MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(4, vec![(0, 1), (2, 3)])); + MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap()); let reduction: ReductionMatchingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -150,10 +161,9 @@ fn test_solution_extraction() { #[test] fn test_ilp_structure() { - let problem = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4)], - )); + let problem = MaximumMatching::<_, i64>::unit_weights( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(), + ); let reduction: ReductionMatchingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -167,7 +177,7 @@ fn test_ilp_structure() { #[test] fn test_empty_graph() { // Graph with no edges: empty matching - let problem = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![])); + let problem = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![]).unwrap()); let reduction: ReductionMatchingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -182,10 +192,9 @@ fn test_empty_graph() { #[test] fn test_k4_perfect_matching() { // Complete graph K4: can have perfect matching (2 edges covering all 4 vertices) - let problem = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )); + let problem = MaximumMatching::<_, i64>::unit_weights( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), + ); let reduction: ReductionMatchingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -210,8 +219,9 @@ fn test_k4_perfect_matching() { fn test_star_graph() { // Star graph with center vertex 0 connected to 1, 2, 3 // Max matching = 1 (only one edge can be selected) - let problem = - MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)])); + let problem = MaximumMatching::<_, i64>::unit_weights( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(), + ); let reduction: ReductionMatchingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -228,10 +238,9 @@ fn test_star_graph() { fn test_bipartite_graph() { // Bipartite graph: {0,1} and {2,3} with all cross edges // Max matching = 2 (one perfect matching) - let problem = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new( - 4, - vec![(0, 2), (0, 3), (1, 2), (1, 3)], - )); + let problem = MaximumMatching::<_, i64>::unit_weights( + SimpleGraph::new(4, vec![(0, 2), (0, 3), (1, 2), (1, 3)]).unwrap(), + ); let reduction: ReductionMatchingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -246,8 +255,9 @@ fn test_bipartite_graph() { #[test] fn test_solve_via_ilp_pipeline() { - let problem = - MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = MaximumMatching::<_, i64>::unit_weights( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), + ); let ilp_solver = ILPSolver::new(); let solution = ilp_solver @@ -260,8 +270,9 @@ fn test_solve_via_ilp_pipeline() { #[test] fn test_maximummatching_to_ilp_bf_vs_ilp() { - let problem = - MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = MaximumMatching::<_, i64>::unit_weights( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), + ); let reduction: ReductionMatchingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); diff --git a/src/unit_tests/rules/maximummatching_maximumsetpacking.rs b/src/unit_tests/rules/maximummatching_maximumsetpacking.rs index e2a96c257..39a0acbd1 100644 --- a/src/unit_tests/rules/maximummatching_maximumsetpacking.rs +++ b/src/unit_tests/rules/maximummatching_maximumsetpacking.rs @@ -1,16 +1,16 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Max; -include!("../jl_helpers.rs"); #[test] fn test_maximummatching_to_maximumsetpacking_closed_loop() { // Path graph 0-1-2 let matching = - MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&matching).expect("reduction should succeed"); let sp = reduction.target_problem(); @@ -28,9 +28,10 @@ fn test_maximummatching_to_maximumsetpacking_closed_loop() { fn test_matching_to_setpacking_weighted() { // Weighted edges: heavy edge should win over multiple light edges let matching = MaximumMatching::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 3)]).unwrap(), vec![100, 1, 1], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&matching).expect("reduction should succeed"); let sp = reduction.target_problem(); @@ -55,8 +56,9 @@ fn test_matching_to_setpacking_weighted() { #[test] fn test_matching_to_setpacking_solution_extraction() { - let matching = - MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let matching = MaximumMatching::<_, i64>::unit_weights( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), + ); let reduction = ReduceTo::>::reduce_to(&matching).expect("reduction should succeed"); @@ -72,7 +74,7 @@ fn test_matching_to_setpacking_solution_extraction() { #[test] fn test_matching_to_setpacking_empty() { // Graph with no edges - let matching = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![])); + let matching = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![]).unwrap()); let reduction = ReduceTo::>::reduce_to(&matching).expect("reduction should succeed"); let sp = reduction.target_problem(); @@ -82,7 +84,8 @@ fn test_matching_to_setpacking_empty() { #[test] fn test_matching_to_setpacking_single_edge() { - let matching = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(2, vec![(0, 1)])); + let matching = + MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(2, vec![(0, 1)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&matching).expect("reduction should succeed"); let sp = reduction.target_problem(); @@ -101,7 +104,7 @@ fn test_matching_to_setpacking_single_edge() { fn test_matching_to_setpacking_disjoint_edges() { // Two disjoint edges: 0-1 and 2-3 let matching = - MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(4, vec![(0, 1), (2, 3)])); + MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&matching).expect("reduction should succeed"); let sp = reduction.target_problem(); @@ -115,8 +118,9 @@ fn test_matching_to_setpacking_disjoint_edges() { #[test] fn test_reduction_structure() { - let matching = - MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3)])); + let matching = MaximumMatching::<_, i64>::unit_weights( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), + ); let reduction = ReduceTo::>::reduce_to(&matching).expect("reduction should succeed"); let sp = reduction.target_problem(); @@ -128,8 +132,9 @@ fn test_reduction_structure() { #[test] fn test_matching_to_setpacking_star() { // Star graph: center vertex 0 connected to 1, 2, 3 - let matching = - MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)])); + let matching = MaximumMatching::<_, i64>::unit_weights( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(), + ); let reduction = ReduceTo::>::reduce_to(&matching).expect("reduction should succeed"); let sp = reduction.target_problem(); @@ -170,9 +175,10 @@ fn test_jl_parity_matching_to_setpacking() { let edges: Vec<(usize, usize)> = weighted_edges.iter().map(|&(u, v, _)| (u, v)).collect(); let weights: Vec = weighted_edges.into_iter().map(|(_, _, w)| w).collect(); let source = MaximumMatching::new( - SimpleGraph::new(inst["num_vertices"].as_u64().unwrap() as usize, edges), + SimpleGraph::new(inst["num_vertices"].as_u64().unwrap() as usize, edges).unwrap(), weights, - ); + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let solver = BruteForce::new(); diff --git a/src/unit_tests/rules/maximumsetpacking_ilp.rs b/src/unit_tests/rules/maximumsetpacking_ilp.rs index 8a7e838e8..d0e6b68e7 100644 --- a/src/unit_tests/rules/maximumsetpacking_ilp.rs +++ b/src/unit_tests/rules/maximumsetpacking_ilp.rs @@ -142,3 +142,63 @@ fn test_maximumsetpacking_to_ilp_bf_vs_ilp() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } + +#[test] +fn extraction_maps_feasible_witnesses_through_typed_and_dynamic_paths() { + use crate::rules::{DynReductionResult, ReductionGraph}; + use serde_json::json; + + let source = MaximumSetPacking::with_weights(vec![vec![0]], vec![1i64]).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let graph = ReductionGraph::new(); + let path = graph + .find_all_paths( + MaximumSetPacking::::NAME, + &ReductionGraph::variant_to_map(&MaximumSetPacking::::variant()), + ILP::::NAME, + &ReductionGraph::variant_to_map(&ILP::::variant()), + ) + .into_iter() + .find(|path| path.len() == 1) + .unwrap(); + let chain = graph.reduce_along_path(&path, &source).unwrap().unwrap(); + assert_eq!(reduction.extract_solution(&vec![1]).unwrap(), vec![true]); + let extracted = reduction.extract_solution_dyn(&vec![1i64]).unwrap(); + assert_eq!(*extracted.downcast::>().unwrap(), vec![true]); + // An unselected set is feasible even though it is not optimal. + assert_eq!(reduction.extract_solution(&vec![0]).unwrap(), vec![false]); + assert_eq!( + chain.extract_solution_json(json!([0])).unwrap(), + json!([false]) + ); +} + +#[test] +fn parameter_upper_bounds_cover_single_and_shared_elements() { + use crate::parameters::ParameterRelation; + use crate::rules::registry::ReductionEntry; + let entry = inventory::iter:: + .into_iter() + .find(|entry| { + entry.source_name == MaximumSetPacking::::NAME + && entry.target_name == ILP::::NAME + && (entry.source_variant_fn)() == MaximumSetPacking::::variant() + && (entry.target_variant_fn)() == ILP::::variant() + }) + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let transform = contract.transform().unwrap(); + assert_eq!(transform.relation(), ParameterRelation::UpperBound); + for (sets, constraints) in [ + (vec![vec![0]], 0), + (vec![vec![0, 1], vec![1, 2], vec![2, 3]], 2), + ] { + let source = MaximumSetPacking::::new(sets); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let actual = reduction.target_problem().parameters(); + let declared = transform.evaluate(&source.parameters()).unwrap(); + assert_eq!(actual.get("num_constraints"), Some(constraints)); + assert_eq!(actual.get("num_vars"), declared.get("num_vars")); + assert!(actual.get("num_constraints").unwrap() <= declared.get("num_constraints").unwrap()); + } +} diff --git a/src/unit_tests/rules/maximumsetpacking_qubo.rs b/src/unit_tests/rules/maximumsetpacking_qubo.rs index c523def04..503e7c213 100644 --- a/src/unit_tests/rules/maximumsetpacking_qubo.rs +++ b/src/unit_tests/rules/maximumsetpacking_qubo.rs @@ -64,7 +64,7 @@ fn test_setpacking_to_qubo_structure() { let qubo = reduction.target_problem(); // QUBO should have same number of variables as sets - assert_eq!(qubo.num_variables(), 3); + assert_eq!(qubo.num_variables().unwrap(), 3); } #[test] @@ -106,7 +106,7 @@ fn test_setpacking_to_qubo_penalty_strict_at_large_weights() { let source = MaximumSetPacking::with_weights(vec![vec![0], vec![0]], vec![weight, weight]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - assert!(reduction.target_problem().matrix()[0][1] > weight); + assert!(reduction.target_problem().matrix()[[0, 1]] > weight); crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target( &source, &reduction, diff --git a/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs b/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs index e3725ece6..0dbca4cda 100644 --- a/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs @@ -10,12 +10,13 @@ use crate::types::Min; /// Small instance: 4 vertices, 5 edges. fn small_instance() -> MinimumCapacitatedSpanningTree { MinimumCapacitatedSpanningTree::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]).unwrap(), vec![2, 3, 1, 1, 2], // edge weights 0, // root vec![0, 1, 1, 1], // requirements 2, // capacity ) + .unwrap() } /// Canonical instance from issue #901: 5 vertices, 8 edges. @@ -33,12 +34,14 @@ fn canonical_instance() -> MinimumCapacitatedSpanningTree { (2, 4), (3, 4), ], - ), + ) + .unwrap(), vec![2, 1, 4, 3, 1, 2, 3, 1], 0, vec![0, 1, 1, 1, 1], 3, ) + .unwrap() } #[test] @@ -102,6 +105,13 @@ fn test_solution_extraction_reads_edge_selector_prefix() { target_solution[1] = 1; // edge (0,2) target_solution[3] = 1; // edge (1,3) + // Requirement and connectivity flows run toward root 0. + for offset in [5, 15] { + target_solution[offset + 1] = 2; // 1 -> 0 + target_solution[offset + 3] = 1; // 2 -> 0 + target_solution[offset + 7] = 1; // 3 -> 1 + } + assert_eq!( reduction.extract_solution(&target_solution).unwrap(), vec![true, true, false, true, false] @@ -121,12 +131,13 @@ fn test_minimumcapacitatedspanningtree_to_ilp_star_tree() { // Star from root 0: all edges directly from root. // With capacity >= max single requirement, star is always valid. let problem = MinimumCapacitatedSpanningTree::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(), vec![1, 1, 1], 0, vec![0, 1, 1, 1], 1, // capacity = 1 forces star tree - ); + ) + .unwrap(); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -141,12 +152,13 @@ fn test_minimumcapacitatedspanningtree_to_ilp_path_graph() { // Path 0-1-2-3, root=0, capacity=3, requirements=[0,1,1,1] // Only spanning tree is the path: subtree(1)={1,2,3}->req=3<=3 OK let problem = MinimumCapacitatedSpanningTree::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![2, 3, 1], 0, vec![0, 1, 1, 1], 3, - ); + ) + .unwrap(); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -159,13 +171,17 @@ fn test_minimumcapacitatedspanningtree_to_ilp_path_graph() { #[test] fn test_zero_requirement_vertex_still_must_be_connected() { let problem = MinimumCapacitatedSpanningTree::new( - SimpleGraph::new(4, vec![(0, 1), (1, 3), (0, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 3), (0, 3)]).unwrap(), vec![1, 1, 1], 0, vec![0, 1, 0, 1], 2, - ); + ) + .unwrap(); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } diff --git a/src/unit_tests/rules/minimumcostmaximumflow_minimumcostcirculation.rs b/src/unit_tests/rules/minimumcostmaximumflow_minimumcostcirculation.rs index 8bdc090ed..02f238f7d 100644 --- a/src/unit_tests/rules/minimumcostmaximumflow_minimumcostcirculation.rs +++ b/src/unit_tests/rules/minimumcostmaximumflow_minimumcostcirculation.rs @@ -10,7 +10,7 @@ use crate::types::Min; /// Max-flow value = 3, min cost among value-3 flows = 7. fn canonical_source() -> MinimumCostMaximumFlow { MinimumCostMaximumFlow::new( - DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]), + DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]).unwrap(), 0, 3, vec![2, 1, 1, 1, 2], @@ -64,7 +64,7 @@ fn test_minimumcostmaximumflow_to_minimumcostcirculation_bottleneck() { // flow value = 1. Two paths 1->3 (cost 1) and 1->2->3 (cost 2+3=5) // ensure the cheaper path is selected. let source = MinimumCostMaximumFlow::new( - DirectedGraph::new(4, vec![(0, 1), (1, 2), (1, 3), (2, 3)]), + DirectedGraph::new(4, vec![(0, 1), (1, 2), (1, 3), (2, 3)]).unwrap(), 0, 3, vec![1, 1, 1, 1], @@ -92,7 +92,7 @@ fn test_minimumcostmaximumflow_to_minimumcostcirculation_parallel_arcs() { // Parallel arcs with different costs from 0 to 1, single sink arc. // The cheaper parallel arc must be preferred. let source = MinimumCostMaximumFlow::new( - DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2)]).unwrap(), 0, 2, vec![1, 1, 1], @@ -128,7 +128,7 @@ fn test_minimumcostmaximumflow_to_minimumcostcirculation_unused_low_cost_arc() { // because vertex 2 has no out-arc to t. The reduction must still // produce the correct projection. let source = MinimumCostMaximumFlow::new( - DirectedGraph::new(4, vec![(0, 1), (1, 2), (1, 3)]), + DirectedGraph::new(4, vec![(0, 1), (1, 2), (1, 3)]).unwrap(), 0, 3, vec![1, 1, 1], @@ -147,7 +147,7 @@ fn test_minimumcostmaximumflow_to_minimumcostcirculation_unused_low_cost_arc() { fn test_minimumcostmaximumflow_to_minimumcostcirculation_zero_capacity_arc() { // A zero-capacity arc must remain feasible but contribute nothing. let source = MinimumCostMaximumFlow::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), 0, 2, vec![1, 1, 0], @@ -176,7 +176,7 @@ fn test_minimumcostmaximumflow_to_minimumcostcirculation_zero_capacity_arc() { #[test] fn test_minimumcostmaximumflow_to_minimumcostcirculation_reports_overflow() { let capacity_overflow = MinimumCostMaximumFlow::new( - DirectedGraph::new(3, vec![(0, 1), (0, 2)]), + DirectedGraph::new(3, vec![(0, 1), (0, 2)]).unwrap(), 0, 2, vec![i64::MAX, 1], @@ -188,7 +188,7 @@ fn test_minimumcostmaximumflow_to_minimumcostcirculation_reports_overflow() { )); let cost_overflow = MinimumCostMaximumFlow::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 0, 2, vec![1, 1], @@ -210,7 +210,7 @@ fn test_minimumcostmaximumflow_to_minimumcostcirculation_value_priority_over_cos // (cheaper in raw cost but lower value), so the lex-optimum is // value 2. let source = MinimumCostMaximumFlow::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 0, 2, vec![2, 2], @@ -239,13 +239,9 @@ fn test_minimumcostmaximumflow_to_minimumcostcirculation_extract_solution_length let source = canonical_source(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); - // Provide a dummy target config of the right length; extract_solution - // must truncate to num_original_arcs. + // A value-3 flow closes through the added sink-to-source return arc. let m = source.num_arcs(); - let mut padded = vec![0_usize; m + 1]; - for (i, v) in padded.iter_mut().enumerate().take(m) { - *v = i % 2; - } + let padded = vec![2_usize, 1, 1, 1, 2, 3]; let extracted = reduction.extract_solution(&padded).unwrap(); assert_eq!(extracted.len(), m); assert_eq!(extracted, padded[..m].to_vec()); diff --git a/src/unit_tests/rules/minimumcoveringbycliques_ilp.rs b/src/unit_tests/rules/minimumcoveringbycliques_ilp.rs index 4e322beef..5949bddbf 100644 --- a/src/unit_tests/rules/minimumcoveringbycliques_ilp.rs +++ b/src/unit_tests/rules/minimumcoveringbycliques_ilp.rs @@ -8,7 +8,7 @@ use crate::types::Min; #[test] fn test_reduction_shape_on_path_p3() { - let source = MinimumCoveringByCliques::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let source = MinimumCoveringByCliques::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction: ReductionMinimumCoveringByCliquesToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -20,10 +20,9 @@ fn test_reduction_shape_on_path_p3() { #[test] fn test_minimumcoveringbycliques_to_ilp_closed_loop() { - let source = MinimumCoveringByCliques::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (2, 3)], - )); + let source = MinimumCoveringByCliques::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (2, 3)]).unwrap(), + ); let reduction: ReductionMinimumCoveringByCliquesToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); @@ -41,7 +40,7 @@ fn test_minimumcoveringbycliques_to_ilp_closed_loop() { #[test] fn test_minimumcoveringbycliques_to_ilp_empty_graph() { - let source = MinimumCoveringByCliques::new(SimpleGraph::new(3, vec![])); + let source = MinimumCoveringByCliques::new(SimpleGraph::new(3, vec![]).unwrap()); let reduction: ReductionMinimumCoveringByCliquesToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -57,10 +56,9 @@ fn test_minimumcoveringbycliques_to_ilp_empty_graph() { #[test] fn test_minimumcoveringbycliques_to_ilp_bf_vs_ilp() { - let source = MinimumCoveringByCliques::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (2, 3)], - )); + let source = MinimumCoveringByCliques::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (2, 3)]).unwrap(), + ); let reduction: ReductionMinimumCoveringByCliquesToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); diff --git a/src/unit_tests/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs b/src/unit_tests/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs index 0737f7f5d..6ed57b5f7 100644 --- a/src/unit_tests/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs +++ b/src/unit_tests/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs @@ -6,8 +6,9 @@ use crate::types::Min; #[test] fn test_minimumcoveringbycliques_to_minimumintersectiongraphbasis_closed_loop() { - let source = - MinimumCoveringByCliques::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)])); + let source = MinimumCoveringByCliques::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]).unwrap(), + ); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); @@ -20,8 +21,9 @@ fn test_minimumcoveringbycliques_to_minimumintersectiongraphbasis_closed_loop() #[test] fn test_minimumcoveringbycliques_to_minimumintersectiongraphbasis_structure_identity() { - let source = - MinimumCoveringByCliques::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)])); + let source = MinimumCoveringByCliques::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]).unwrap(), + ); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -33,8 +35,9 @@ fn test_minimumcoveringbycliques_to_minimumintersectiongraphbasis_structure_iden #[test] fn test_minimumcoveringbycliques_to_minimumintersectiongraphbasis_issue_example_extraction() { - let source = - MinimumCoveringByCliques::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)])); + let source = MinimumCoveringByCliques::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]).unwrap(), + ); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -50,7 +53,7 @@ fn test_minimumcoveringbycliques_to_minimumintersectiongraphbasis_issue_example_ #[test] fn test_minimumcoveringbycliques_to_minimumintersectiongraphbasis_invalid_target_rejected() { - let source = MinimumCoveringByCliques::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let source = MinimumCoveringByCliques::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -60,19 +63,11 @@ fn test_minimumcoveringbycliques_to_minimumintersectiongraphbasis_invalid_target target.evaluate(&invalid_target_solution).unwrap(), Min(None) ); - - let error = reduction - .extract_solution(&invalid_target_solution) - .unwrap_err(); - assert_eq!( - error.to_string(), - "target configuration is not a valid intersection graph basis" - ); } #[test] fn test_minimumcoveringbycliques_to_minimumintersectiongraphbasis_empty_graph() { - let source = MinimumCoveringByCliques::new(SimpleGraph::new(3, vec![])); + let source = MinimumCoveringByCliques::new(SimpleGraph::new(3, vec![]).unwrap()); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs b/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs index 1f31f58a5..6817ee450 100644 --- a/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs +++ b/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs @@ -9,12 +9,13 @@ use crate::traits::Problem; fn small_instance() -> MinimumCutIntoBoundedSets { // Path graph 0-1-2-3, unit weights, s=0, t=3, B=3 MinimumCutIntoBoundedSets::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1, 1, 1], 0, 3, 3, ) + .unwrap() } #[test] @@ -52,12 +53,14 @@ fn test_larger_instance() { SimpleGraph::new( 6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 2), (3, 5)], - ), + ) + .unwrap(), vec![1, 2, 1, 2, 1, 2, 1], 0, 5, 4, - ); + ) + .unwrap(); let reduction: ReductionMinCutBSToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); assert_bf_vs_ilp(&source, &reduction); diff --git a/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs b/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs index fe188e749..943a4e257 100644 --- a/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs +++ b/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs @@ -96,8 +96,14 @@ fn test_minimumdiscreteplanarinversekinematics_to_qubo_empty_allowed_pairs() { assert!(solver.solve(&source).unwrap().is_none()); assert!(!qubo_solutions.is_empty(), "QUBO solver found no solutions"); for target_solution in qubo_solutions { - let extracted = reduction.extract_solution(&target_solution).unwrap(); - assert_eq!(source.evaluate(&extracted).unwrap(), Min(None)); + let value = reduction + .target_problem() + .evaluate(&target_solution) + .unwrap(); + assert_eq!( + crate::rules::AggregateReductionResult::extract_value(&reduction, value), + Min(None) + ); } } @@ -115,7 +121,7 @@ fn test_minimumdiscreteplanarinversekinematics_to_qubo_canonical_example_spec() "MinimumDiscretePlanarInverseKinematics" ); assert_eq!(example.target.problem, "QUBO"); - assert_eq!(example.target.instance["num_vars"], 4); + assert_eq!(example.target.instance["matrix"]["nrows"], 4); assert_eq!( example.solutions[0].source_config, serde_json::json!([0, 1]) @@ -125,3 +131,89 @@ fn test_minimumdiscreteplanarinversekinematics_to_qubo_canonical_example_spec() serde_json::json!([true, false, false, true]) ); } + +#[test] +fn optimum_energy_recovers_distance_and_infeasibility() { + for source in [ + worked_example(), + MinimumDiscretePlanarInverseKinematics::new( + vec![1.0, 1.0, 1.0], + (0.0, 0.0), + vec![vec![0.0, PI]; 3], + vec![vec![(0, 0)], vec![(1, 0)]], + ) + .unwrap(), + ] { + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let entry = inventory::iter:: + .into_iter() + .find(|entry| { + entry.source_name == "MinimumDiscretePlanarInverseKinematics" + && entry.target_name == "QUBO" + }) + .unwrap(); + let chain = + crate::rules::ReductionChain::execute(&source, &[entry.reduce_fn.unwrap()]).unwrap(); + + let solver = BruteForce::new(); + let expected = solver + .solve(&source) + .unwrap() + .map(|solution| source.evaluate(&solution).unwrap().0.unwrap()); + for solution in solver + .find_all_witnesses(reduction.target_problem()) + .unwrap() + { + let completed = crate::solvers::complete_reduction( + &source, + &chain, + &crate::solvers::SolveOutcome::Optimal { + solution: serde_json::to_value(&solution).unwrap(), + evaluation: String::new(), + }, + ) + .unwrap(); + assert_eq!( + matches!(completed, crate::solvers::SolveOutcome::Optimal { .. }), + expected.is_some() + ); + let recovered = crate::rules::AggregateReductionResult::extract_value( + &reduction, + reduction.target_problem().evaluate(&solution).unwrap(), + ) + .0; + match (expected, recovered) { + (Some(expected), Some(actual)) => { + assert!((actual - expected).abs() < EPS); + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Min(Some(expected)) + ); + } + (None, None) => {} + other => panic!("source and recovered outcomes disagree: {other:?}"), + } + } + assert_eq!( + crate::rules::AggregateReductionResult::extract_value(&reduction, Min(None)), + Min(None) + ); + } +} + +#[test] +fn nonfinite_energy_relation_is_a_construction_error() { + let source = MinimumDiscretePlanarInverseKinematics::new( + vec![1e200], + (0.0, 0.0), + vec![vec![0.0]], + vec![], + ) + .unwrap(); + assert!(matches!( + ReduceTo::>::reduce_to(&source), + Err(crate::rules::ReductionError::NonFiniteResult { .. }) + )); +} diff --git a/src/unit_tests/rules/minimumdominatingset_ilp.rs b/src/unit_tests/rules/minimumdominatingset_ilp.rs index deddfa504..38f3059aa 100644 --- a/src/unit_tests/rules/minimumdominatingset_ilp.rs +++ b/src/unit_tests/rules/minimumdominatingset_ilp.rs @@ -7,9 +7,10 @@ use crate::types::Min; fn test_reduction_creates_valid_ilp() { // Triangle graph: 3 vertices, 3 edges let problem = MinimumDominatingSet::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); let reduction: ReductionDSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -32,7 +33,9 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_reduction_weighted() { - let problem = MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![5, 10, 15]); + let problem = + MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![5, 10, 15]) + .unwrap(); let reduction: ReductionDSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -50,9 +53,10 @@ fn test_minimumdominatingset_to_ilp_closed_loop() { // Star graph: center vertex 0 connected to all others // Minimum dominating set is just the center (weight 1) let problem = MinimumDominatingSet::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let reduction: ReductionDSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -84,9 +88,10 @@ fn test_minimumdominatingset_to_ilp_closed_loop() { fn test_ilp_solution_equals_brute_force_path() { // Path graph 0-1-2-3-4: min DS = 2 (e.g., vertices 1 and 3) let problem = MinimumDominatingSet::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(), vec![1i64; 5], - ); + ) + .unwrap(); let reduction: ReductionDSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -115,9 +120,10 @@ fn test_ilp_solution_equals_brute_force_weighted() { // Star with heavy center: prefer selecting all leaves (total weight 3) // over center (weight 100) let problem = MinimumDominatingSet::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(), vec![100, 1, 1, 1], - ); + ) + .unwrap(); let reduction: ReductionDSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -141,8 +147,11 @@ fn test_ilp_solution_equals_brute_force_weighted() { #[test] fn test_solution_extraction() { - let problem = - MinimumDominatingSet::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), vec![1i64; 4]); + let problem = MinimumDominatingSet::new( + SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(), + vec![1i64; 4], + ) + .unwrap(); let reduction: ReductionDSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -158,9 +167,10 @@ fn test_solution_extraction() { #[test] fn test_ilp_structure() { let problem = MinimumDominatingSet::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(), vec![1i64; 5], - ); + ) + .unwrap(); let reduction: ReductionDSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -172,7 +182,9 @@ fn test_ilp_structure() { #[test] fn test_isolated_vertices() { // Graph with isolated vertex 2: it must be in the dominating set - let problem = MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); + let problem = + MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]) + .unwrap(); let reduction: ReductionDSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -191,9 +203,10 @@ fn test_isolated_vertices() { fn test_complete_graph() { // Complete graph K4: min DS = 1 (any vertex dominates all) let problem = MinimumDominatingSet::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let reduction: ReductionDSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -209,7 +222,8 @@ fn test_complete_graph() { #[test] fn test_single_vertex() { // Single vertex with no edges: must be in dominating set - let problem = MinimumDominatingSet::new(SimpleGraph::new(1, vec![]), vec![1i64; 1]); + let problem = + MinimumDominatingSet::new(SimpleGraph::new(1, vec![]).unwrap(), vec![1i64; 1]).unwrap(); let reduction: ReductionDSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -229,9 +243,10 @@ fn test_cycle_graph() { // Cycle C5: 0-1-2-3-4-0 // Minimum dominating set size = 2 let problem = MinimumDominatingSet::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(), vec![1i64; 5], - ); + ) + .unwrap(); let reduction: ReductionDSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -254,9 +269,10 @@ fn test_cycle_graph() { #[test] fn test_minimumdominatingset_to_ilp_bf_vs_ilp() { let problem = MinimumDominatingSet::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let reduction: ReductionDSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); diff --git a/src/unit_tests/rules/minimumedgecostflow_ilp.rs b/src/unit_tests/rules/minimumedgecostflow_ilp.rs index 683d75059..f75e7bc28 100644 --- a/src/unit_tests/rules/minimumedgecostflow_ilp.rs +++ b/src/unit_tests/rules/minimumedgecostflow_ilp.rs @@ -7,13 +7,14 @@ use crate::types::Min; fn issue_instance() -> MinimumEdgeCostFlow { MinimumEdgeCostFlow::new( - DirectedGraph::new(5, vec![(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4)]), + DirectedGraph::new(5, vec![(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4)]).unwrap(), vec![3, 1, 2, 0, 0, 0], vec![2, 2, 2, 2, 2, 2], 0, 4, 3, ) + .unwrap() } fn small_instance() -> MinimumEdgeCostFlow { @@ -22,25 +23,27 @@ fn small_instance() -> MinimumEdgeCostFlow { // Arc 1: (1,2) cap=2, price=3 // R=1 → cost = 5+3 = 8 MinimumEdgeCostFlow::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![5, 3], vec![2, 2], 0, 2, 1, ) + .unwrap() } fn infeasible_instance() -> MinimumEdgeCostFlow { // Cannot route 2 units through capacity-1 arcs MinimumEdgeCostFlow::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1, 1], vec![1, 1], 0, 2, 2, ) + .unwrap() } #[test] @@ -109,8 +112,9 @@ fn test_minimumedgecostflow_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionMECFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/minimumexternalmacrodatacompression_ilp.rs b/src/unit_tests/rules/minimumexternalmacrodatacompression_ilp.rs index 2892f3b53..5beb07c3c 100644 --- a/src/unit_tests/rules/minimumexternalmacrodatacompression_ilp.rs +++ b/src/unit_tests/rules/minimumexternalmacrodatacompression_ilp.rs @@ -8,7 +8,7 @@ use crate::types::Min; fn test_emdc_to_ilp_closed_loop() { // s = "ab" (len 2), alphabet {a,b}, h=2 // Optimal: uncompressed, cost = 2 - let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() @@ -27,7 +27,7 @@ fn test_emdc_to_ilp_compression_wins() { // (pointer cost h=2, so (h-1)*3 = 3, total = 6+3+3 = 12) // Uncompressed: 18 let s: Vec = (0..6).cycle().take(18).collect(); - let problem = MinimumExternalMacroDataCompression::new(6, s, 2); + let problem = MinimumExternalMacroDataCompression::new(6, s, 2).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() @@ -42,7 +42,7 @@ fn test_emdc_to_ilp_compression_wins() { #[test] fn test_emdc_to_ilp_structure() { // s = "ab" (len 2), alphabet {a,b} (k=2), h=2 - let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -70,7 +70,7 @@ fn test_emdc_to_ilp_structure() { #[test] fn test_emdc_to_ilp_empty() { // Empty string: cost should be 0 - let problem = MinimumExternalMacroDataCompression::new(2, vec![], 1); + let problem = MinimumExternalMacroDataCompression::new(2, vec![], 1).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -86,7 +86,7 @@ fn test_emdc_to_ilp_empty() { #[test] fn test_emdc_to_ilp_bf_vs_ilp() { // Small instance: s="ab", alphabet {a,b}, h=2 - let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2); + let problem = MinimumExternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } @@ -96,7 +96,7 @@ fn test_emdc_to_ilp_single_char() { // s = "a" (len 1), alphabet {a} (k=1), h=1 // Uncompressed: cost = 0+1+0 = 1. With D="a"(1), C=ptr(0,1)(1, 1 ptr): cost = 1+1+0 = 2. // So uncompressed is optimal. - let problem = MinimumExternalMacroDataCompression::new(1, vec![0], 1); + let problem = MinimumExternalMacroDataCompression::new(1, vec![0], 1).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() @@ -115,7 +115,7 @@ fn test_emdc_to_ilp_repeated_string() { // D="aaa"(3), C=ptr(0,3): cost = 3+1+0 = 4. // D="aa"(2), C=ptr(0,1) ptr(0,2): cost = 2+2+0 = 4. // Uncompressed is best at 3. - let problem = MinimumExternalMacroDataCompression::new(1, vec![0, 0, 0], 1); + let problem = MinimumExternalMacroDataCompression::new(1, vec![0, 0, 0], 1).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() diff --git a/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs b/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs index f2400cdfc..0d11cd298 100644 --- a/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs +++ b/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs @@ -22,6 +22,7 @@ fn issue_problem() -> MinimumFaultDetectionTestSet { vec![0, 1], vec![5, 6], ) + .unwrap() } #[test] @@ -70,7 +71,7 @@ fn test_minimumfaultdetectiontestset_to_ilp_closed_loop() { #[test] fn test_reduction_is_infeasible_when_an_internal_vertex_has_no_covering_pair() { - let problem = MinimumFaultDetectionTestSet::new(3, vec![], vec![0], vec![2]); + let problem = MinimumFaultDetectionTestSet::new(3, vec![], vec![0], vec![2]).unwrap(); let reduction: ReductionMFDTSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -83,12 +84,15 @@ fn test_reduction_is_infeasible_when_an_internal_vertex_has_no_covering_pair() { assert_eq!(problem.evaluate(&vec![vec![false]]).unwrap(), Min(None)); assert_eq!(problem.evaluate(&vec![vec![true]]).unwrap(), Min(None)); - assert!(ILPSolver::new().solve(ilp).is_err()); + assert_eq!( + ILPSolver::new().solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] fn test_reduction_handles_instances_without_internal_vertices() { - let problem = MinimumFaultDetectionTestSet::new(2, vec![(0, 1)], vec![0], vec![1]); + let problem = MinimumFaultDetectionTestSet::new(2, vec![(0, 1)], vec![0], vec![1]).unwrap(); let reduction: ReductionMFDTSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs b/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs index 09413a71b..4b977d9cd 100644 --- a/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs +++ b/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs @@ -8,8 +8,8 @@ use crate::types::Min; fn test_reduction_creates_valid_ilp() { // Simple 3-cycle: 0 -> 1 -> 2 -> 0 // m=3 arcs, n=3 vertices → 6 variables, m+m+n = 9 constraints - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]).unwrap(); let reduction: ReductionFASToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -29,8 +29,8 @@ fn test_reduction_creates_valid_ilp() { fn test_minimumfeedbackarcset_to_ilp_bf_vs_ilp() { // Triangle cycle: 0 -> 1 -> 2 -> 0 // FAS = 1 (remove any single arc to break the cycle) - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]).unwrap(); let reduction: ReductionFASToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -55,8 +55,8 @@ fn test_minimumfeedbackarcset_to_ilp_bf_vs_ilp() { #[test] fn test_solution_extraction() { // Verify that extraction correctly takes first m arc values - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]).unwrap(); let reduction: ReductionFASToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -75,8 +75,8 @@ fn test_solution_extraction() { #[test] fn test_minimumfeedbackarcset_to_ilp_trivial() { // DAG: 0 -> 1 -> 2 (no cycles, FAS = 0) - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 2]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 2]).unwrap(); let reduction: ReductionFASToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs b/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs index 8b47862ae..aa12daf0d 100644 --- a/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs +++ b/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs @@ -7,8 +7,8 @@ use crate::types::Min; #[test] fn test_reduction_creates_valid_ilp() { // Simple 3-cycle: 0 -> 1 -> 2 -> 0 - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]).unwrap(); let reduction: ReductionMFVSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -24,8 +24,8 @@ fn test_reduction_creates_valid_ilp() { fn test_minimumfeedbackvertexset_to_ilp_closed_loop() { // Simple 3-cycle: 0 -> 1 -> 2 -> 0 // FVS = 1 (remove any single vertex) - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]).unwrap(); let reduction: ReductionMFVSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -73,8 +73,8 @@ fn test_cycle_of_triangles() { (5, 8), (8, 2), // more inter-triangle arcs ]; - let graph = DirectedGraph::new(9, arcs); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 9]); + let graph = DirectedGraph::new(9, arcs).unwrap(); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 9]).unwrap(); let reduction: ReductionMFVSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -98,8 +98,8 @@ fn test_cycle_of_triangles() { #[test] fn test_dag_no_removal() { // DAG: 0 -> 1 -> 2 (no cycles, FVS = 0) - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]).unwrap(); let reduction: ReductionMFVSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -116,8 +116,8 @@ fn test_dag_no_removal() { #[test] fn test_single_vertex() { // Single vertex, no arcs: FVS = 0 - let graph = DirectedGraph::new(1, vec![]); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64]); + let graph = DirectedGraph::new(1, vec![]).unwrap(); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64]).unwrap(); let reduction: ReductionMFVSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -138,8 +138,8 @@ fn test_single_vertex() { fn test_weighted() { // 3-cycle with different weights: prefer removing the cheapest vertex // Weights: v0=10, v1=1, v2=10 - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackVertexSet::new(graph, vec![10, 1, 10]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackVertexSet::new(graph, vec![10, 1, 10]).unwrap(); let reduction: ReductionMFVSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -164,8 +164,8 @@ fn test_weighted() { fn test_two_disjoint_cycles() { // Two disjoint 2-cycles: 0<->1 and 2<->3 // Need to remove at least 1 from each cycle, FVS = 2 - let graph = DirectedGraph::new(4, vec![(0, 1), (1, 0), (2, 3), (3, 2)]); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 4]); + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 0), (2, 3), (3, 2)]).unwrap(); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 4]).unwrap(); let bf = BruteForce::new(); let bf_solutions = bf.find_all_witnesses(&problem).unwrap(); @@ -186,8 +186,8 @@ fn test_two_disjoint_cycles() { #[test] fn test_solution_extraction() { // Verify that extraction correctly takes first n values - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]).unwrap(); let reduction: ReductionMFVSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -202,8 +202,8 @@ fn test_solution_extraction() { #[test] fn test_minimumfeedbackvertexset_to_ilp_bf_vs_ilp() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]).unwrap(); let reduction: ReductionMFVSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); diff --git a/src/unit_tests/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs b/src/unit_tests/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs index 3712e5c42..443301943 100644 --- a/src/unit_tests/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs +++ b/src/unit_tests/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs @@ -38,9 +38,10 @@ fn test_codegen_start_nodes_cover_self_loops_and_parallel_arcs() { use crate::traits::Problem; use crate::types::{Min, One}; let source = MinimumFeedbackVertexSet::new( - DirectedGraph::new(3, vec![(0, 1), (1, 0), (2, 2), (2, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 0), (2, 2), (2, 2)]).unwrap(), vec![One; 3], - ); + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let target = reduction.target_problem(); @@ -59,7 +60,9 @@ fn test_codegen_empty_graph_and_invalid_orders() { use crate::rules::ReductionResult; use crate::topology::DirectedGraph; use crate::types::One; - let empty = MinimumFeedbackVertexSet::::new(DirectedGraph::new(0, vec![]), vec![]); + let empty = + MinimumFeedbackVertexSet::::new(DirectedGraph::new(0, vec![]).unwrap(), vec![]) + .unwrap(); let reduction = ReduceTo::::reduce_to(&empty).unwrap(); assert_eq!(reduction.target_problem().num_vertices(), 1); assert_eq!( @@ -70,7 +73,9 @@ fn test_codegen_empty_graph_and_invalid_orders() { let reduction = ReduceTo::::reduce_to(&source).unwrap(); for config in [vec![], vec![9; 6], vec![0; 6], vec![1, 0, 2, 3, 4, 5]] { - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } } @@ -106,7 +111,9 @@ fn test_codegen_every_small_evaluation_permutation() { .enumerate() .filter_map(|(i, &arc)| (mask & (1 << i) != 0).then_some(arc)) .collect(); - let source = MinimumFeedbackVertexSet::new(DirectedGraph::new(n, arcs), vec![One; n]); + let source = + MinimumFeedbackVertexSet::new(DirectedGraph::new(n, arcs).unwrap(), vec![One; n]) + .unwrap(); let witness = BruteForce::new().solve(&source).unwrap().unwrap(); let Min(Some(optimum)) = source.evaluate(&witness).unwrap() else { unreachable!() diff --git a/src/unit_tests/rules/minimumgraphbandwidth_ilp.rs b/src/unit_tests/rules/minimumgraphbandwidth_ilp.rs index 9eec5a8d3..36dfa3e6e 100644 --- a/src/unit_tests/rules/minimumgraphbandwidth_ilp.rs +++ b/src/unit_tests/rules/minimumgraphbandwidth_ilp.rs @@ -6,7 +6,8 @@ use crate::traits::Problem; #[test] fn test_reduction_creates_valid_ilp() { // Star S4: 4 vertices, 3 edges - let problem = MinimumGraphBandwidth::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)])); + let problem = + MinimumGraphBandwidth::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap()); let reduction: ReductionMGBToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -18,7 +19,8 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_minimumgraphbandwidth_to_ilp_closed_loop() { // Star S4 - let problem = MinimumGraphBandwidth::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)])); + let problem = + MinimumGraphBandwidth::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap()); // BruteForce on source to verify feasibility let bf = BruteForce::new(); @@ -53,7 +55,8 @@ fn test_minimumgraphbandwidth_to_ilp_closed_loop() { #[test] fn test_minimumgraphbandwidth_to_ilp_path() { // Path P4: 0-1-2-3 (optimal bandwidth = 1) - let problem = MinimumGraphBandwidth::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = + MinimumGraphBandwidth::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let reduction: ReductionMGBToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -73,7 +76,8 @@ fn test_minimumgraphbandwidth_to_ilp_path() { #[test] fn test_minimumgraphbandwidth_to_ilp_bf_vs_ilp() { // Star S4 - let problem = MinimumGraphBandwidth::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)])); + let problem = + MinimumGraphBandwidth::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap()); let reduction: ReductionMGBToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); @@ -82,8 +86,9 @@ fn test_minimumgraphbandwidth_to_ilp_bf_vs_ilp() { #[test] fn test_minimumgraphbandwidth_to_ilp_cycle() { // Cycle C4: 0-1-2-3-0 (optimal bandwidth = 2) - let problem = - MinimumGraphBandwidth::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)])); + let problem = MinimumGraphBandwidth::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(), + ); let reduction: ReductionMGBToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); diff --git a/src/unit_tests/rules/minimumhittingset_ilp.rs b/src/unit_tests/rules/minimumhittingset_ilp.rs index aa15bc9d6..2adc51015 100644 --- a/src/unit_tests/rules/minimumhittingset_ilp.rs +++ b/src/unit_tests/rules/minimumhittingset_ilp.rs @@ -4,7 +4,7 @@ use crate::traits::Problem; #[test] fn test_reduction_creates_valid_ilp() { - let problem = MinimumHittingSet::new(3, vec![vec![0, 1], vec![1, 2]]); + let problem = MinimumHittingSet::new(3, vec![vec![0, 1], vec![1, 2]]).unwrap(); let reduction: ReductionHSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -15,7 +15,7 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_minimumhittingset_to_ilp_bf_vs_ilp() { - let problem = MinimumHittingSet::new(4, vec![vec![0, 1], vec![2, 3], vec![1, 2]]); + let problem = MinimumHittingSet::new(4, vec![vec![0, 1], vec![2, 3], vec![1, 2]]).unwrap(); let reduction: ReductionHSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -32,7 +32,7 @@ fn test_minimumhittingset_to_ilp_bf_vs_ilp() { #[test] fn test_solution_extraction() { - let problem = MinimumHittingSet::new(3, vec![vec![0, 1], vec![1, 2]]); + let problem = MinimumHittingSet::new(3, vec![vec![0, 1], vec![1, 2]]).unwrap(); let reduction: ReductionHSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = vec![0, 1, 0]; @@ -43,7 +43,7 @@ fn test_solution_extraction() { #[test] fn test_minimumhittingset_to_ilp_trivial() { - let problem = MinimumHittingSet::new(0, vec![]); + let problem = MinimumHittingSet::new(0, vec![]).unwrap(); let reduction: ReductionHSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/minimuminternalmacrodatacompression_ilp.rs b/src/unit_tests/rules/minimuminternalmacrodatacompression_ilp.rs index 09908a2b9..55122a368 100644 --- a/src/unit_tests/rules/minimuminternalmacrodatacompression_ilp.rs +++ b/src/unit_tests/rules/minimuminternalmacrodatacompression_ilp.rs @@ -9,7 +9,7 @@ use crate::types::Min; fn test_imdc_to_ilp_closed_loop_simple() { // s = "ab", alphabet {a,b}, h=2 // Optimal: uncompressed, cost=2 - let source = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2); + let source = MinimumInternalMacroDataCompression::new(2, vec![0, 1], 2).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -25,7 +25,7 @@ fn test_imdc_to_ilp_closed_loop_simple() { fn test_imdc_to_ilp_closed_loop_repeated() { // s = "abab", alphabet {a,b}, h=2 // Optimal: cost=4 (uncompressed or pointer, both cost 4) - let source = MinimumInternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2); + let source = MinimumInternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -42,7 +42,7 @@ fn test_imdc_to_ilp_closed_loop_low_pointer_cost() { // s = "abab", alphabet {a,b}, h=1 // With h=1, pointers cost 0 extra: cost = |C| // Optimal with pointer: C=[a,b,ptr(0)], active=3, ptrs=1, cost=3+0=3 - let source = MinimumInternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 1); + let source = MinimumInternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 1).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -59,7 +59,7 @@ fn test_imdc_to_ilp_closed_loop_low_pointer_cost() { #[test] fn test_imdc_to_ilp_empty_string() { - let source = MinimumInternalMacroDataCompression::new(2, vec![], 2); + let source = MinimumInternalMacroDataCompression::new(2, vec![], 2).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); assert_eq!(target.num_variables(), 0); @@ -71,7 +71,7 @@ fn test_imdc_to_ilp_empty_string() { fn test_imdc_to_ilp_single_char() { // s = "a", alphabet {a}, h=2 // Only valid: literal, cost=1 - let source = MinimumInternalMacroDataCompression::new(1, vec![0], 2); + let source = MinimumInternalMacroDataCompression::new(1, vec![0], 2).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -84,7 +84,7 @@ fn test_imdc_to_ilp_single_char() { #[test] fn test_imdc_to_ilp_structure() { // Verify the ILP has the right number of variables - let source = MinimumInternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2); + let source = MinimumInternalMacroDataCompression::new(2, vec![0, 1, 0, 1], 2).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); // n=4 literals + valid ptr triples @@ -103,7 +103,7 @@ fn test_imdc_to_ilp_vs_brute_force() { (2, vec![0, 1, 0], 2), (2, vec![0, 0, 1, 1], 1), ] { - let source = MinimumInternalMacroDataCompression::new(k, s.clone(), h); + let source = MinimumInternalMacroDataCompression::new(k, s.clone(), h).unwrap(); let bf_val_solution = BruteForce::new().solve(&source).unwrap().unwrap(); let bf_val = source.evaluate(&bf_val_solution).unwrap(); diff --git a/src/unit_tests/rules/minimummatrixcover_ilp.rs b/src/unit_tests/rules/minimummatrixcover_ilp.rs index da371feef..950ec637b 100644 --- a/src/unit_tests/rules/minimummatrixcover_ilp.rs +++ b/src/unit_tests/rules/minimummatrixcover_ilp.rs @@ -13,7 +13,8 @@ fn test_minimum_matrix_cover_to_ilp_closed_loop() { vec![3, 0, 0, 2], vec![1, 0, 0, 4], vec![0, 2, 4, 0], - ]); + ]) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_bf_vs_ilp(&problem, &reduction); @@ -28,7 +29,7 @@ fn test_minimum_matrix_cover_to_ilp_closed_loop() { #[test] fn test_minimum_matrix_cover_to_ilp_structure() { - let problem = MinimumMatrixCover::new(vec![vec![0, 3], vec![2, 0]]); + let problem = MinimumMatrixCover::new(vec![vec![0, 3], vec![2, 0]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -54,7 +55,8 @@ fn test_minimum_matrix_cover_to_ilp_bf_vs_ilp() { vec![3, 0, 0, 2], vec![1, 0, 0, 4], vec![0, 2, 4, 0], - ]); + ]) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf_value_solution = BruteForce::new().solve(&problem).unwrap().unwrap(); @@ -72,7 +74,7 @@ fn test_minimum_matrix_cover_to_ilp_bf_vs_ilp() { #[test] fn test_minimum_matrix_cover_to_ilp_2x2() { - let problem = MinimumMatrixCover::new(vec![vec![0, 3], vec![2, 0]]); + let problem = MinimumMatrixCover::new(vec![vec![0, 3], vec![2, 0]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() @@ -86,7 +88,7 @@ fn test_minimum_matrix_cover_to_ilp_2x2() { #[test] fn test_minimum_matrix_cover_to_ilp_1x1() { - let problem = MinimumMatrixCover::new(vec![vec![5]]); + let problem = MinimumMatrixCover::new(vec![vec![5]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -108,7 +110,8 @@ fn test_minimum_matrix_cover_to_ilp_1x1() { fn test_minimum_matrix_cover_to_ilp_diagonal_matrix() { // Diagonal matrix: all off-diagonal entries are 0 // Value is always Σ a_ii (constant), since f(i)²=1 - let problem = MinimumMatrixCover::new(vec![vec![2, 0, 0], vec![0, 3, 0], vec![0, 0, 1]]); + let problem = + MinimumMatrixCover::new(vec![vec![2, 0, 0], vec![0, 3, 0], vec![0, 0, 1]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() @@ -122,7 +125,7 @@ fn test_minimum_matrix_cover_to_ilp_diagonal_matrix() { #[test] fn test_minimum_matrix_cover_to_ilp_asymmetric() { // Non-symmetric matrix - let problem = MinimumMatrixCover::new(vec![vec![0, 5], vec![1, 0]]); + let problem = MinimumMatrixCover::new(vec![vec![0, 5], vec![1, 0]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf_value_solution = BruteForce::new().solve(&problem).unwrap().unwrap(); diff --git a/src/unit_tests/rules/minimummaximalmatching_ilp.rs b/src/unit_tests/rules/minimummaximalmatching_ilp.rs index 342a422fd..c7abd8a02 100644 --- a/src/unit_tests/rules/minimummaximalmatching_ilp.rs +++ b/src/unit_tests/rules/minimummaximalmatching_ilp.rs @@ -7,7 +7,8 @@ use crate::types::Min; #[test] fn test_reduction_creates_valid_ilp() { // Path P4: 4 vertices, 3 edges - let problem = MinimumMaximalMatching::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = + MinimumMaximalMatching::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let reduction: ReductionMMMToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -23,7 +24,8 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_minimummaximalmatching_to_ilp_closed_loop() { // Path P4: optimal minimum maximal matching = 1 edge (center edge (1,2)). - let problem = MinimumMaximalMatching::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = + MinimumMaximalMatching::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let reduction: ReductionMMMToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -47,10 +49,9 @@ fn test_minimummaximalmatching_to_ilp_closed_loop() { #[test] fn test_minimummaximalmatching_to_ilp_path_p6() { // Path P6: optimal = 2 edges. - let problem = MinimumMaximalMatching::new(SimpleGraph::new( - 6, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)], - )); + let problem = MinimumMaximalMatching::new( + SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]).unwrap(), + ); let reduction: ReductionMMMToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -66,7 +67,8 @@ fn test_minimummaximalmatching_to_ilp_path_p6() { #[test] fn test_minimummaximalmatching_to_ilp_triangle() { // Triangle: optimal = 1 (any single edge is maximal). - let problem = MinimumMaximalMatching::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); + let problem = + MinimumMaximalMatching::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap()); let reduction: ReductionMMMToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -82,10 +84,9 @@ fn test_minimummaximalmatching_to_ilp_triangle() { #[test] fn test_minimummaximalmatching_to_ilp_bf_vs_ilp() { - let problem = MinimumMaximalMatching::new(SimpleGraph::new( - 6, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)], - )); + let problem = MinimumMaximalMatching::new( + SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]).unwrap(), + ); let reduction: ReductionMMMToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); @@ -93,7 +94,7 @@ fn test_minimummaximalmatching_to_ilp_bf_vs_ilp() { #[test] fn test_empty_graph() { - let problem = MinimumMaximalMatching::new(SimpleGraph::new(3, vec![])); + let problem = MinimumMaximalMatching::new(SimpleGraph::new(3, vec![]).unwrap()); let reduction: ReductionMMMToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/minimummaximalmatching_maximumachromaticnumber.rs b/src/unit_tests/rules/minimummaximalmatching_maximumachromaticnumber.rs index 3a7f521c6..24942b128 100644 --- a/src/unit_tests/rules/minimummaximalmatching_maximumachromaticnumber.rs +++ b/src/unit_tests/rules/minimummaximalmatching_maximumachromaticnumber.rs @@ -16,7 +16,7 @@ fn t_tree_bipartite() -> BipartiteGraph { // (v1, v2) -> (1, 0) // (v2, v3) -> (1, 1) // (v1, v4) -> (2, 0) - BipartiteGraph::new(3, 2, vec![(0, 0), (1, 0), (1, 1), (2, 0)]) + BipartiteGraph::new(3, 2, vec![(0, 0), (1, 0), (1, 1), (2, 0)]).unwrap() } #[test] @@ -138,8 +138,8 @@ fn test_identity_on_random_bipartite_instances() { // - K_{1,3} (claw / star), and // - the canonical T-tree above. let instances = vec![ - BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]), - BipartiteGraph::new(1, 3, vec![(0, 0), (0, 1), (0, 2)]), + BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]).unwrap(), + BipartiteGraph::new(1, 3, vec![(0, 0), (0, 1), (0, 2)]).unwrap(), t_tree_bipartite(), ]; diff --git a/src/unit_tests/rules/minimummaximalmatching_minimummatrixdomination.rs b/src/unit_tests/rules/minimummaximalmatching_minimummatrixdomination.rs index 9d0a6ab52..1a90df579 100644 --- a/src/unit_tests/rules/minimummaximalmatching_minimummatrixdomination.rs +++ b/src/unit_tests/rules/minimummaximalmatching_minimummatrixdomination.rs @@ -13,14 +13,14 @@ use crate::types::Min; /// left_size = 2, right_size = 3, /// edges = (0,0), (0,1), (0,2), (1,1), (1,2). fn yes_bipartite() -> BipartiteGraph { - BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (0, 2), (1, 1), (1, 2)]) + BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (0, 2), (1, 1), (1, 2)]).unwrap() } /// Build the canonical NO bipartite instance from the issue (a perfect /// matching on 3+3 vertices: L = {l0, l1, l2}, R = {r0, r1, r2}, F = /// {(l0, r0), (l1, r1), (l2, r2)}). fn no_bipartite() -> BipartiteGraph { - BipartiteGraph::new(3, 3, vec![(0, 0), (1, 1), (2, 2)]) + BipartiteGraph::new(3, 3, vec![(0, 0), (1, 1), (2, 2)]).unwrap() } #[test] @@ -219,9 +219,9 @@ fn test_identity_on_random_bipartite_instances() { let instances = vec![ // K_{1, 3} (star). - BipartiteGraph::new(1, 3, vec![(0, 0), (0, 1), (0, 2)]), + BipartiteGraph::new(1, 3, vec![(0, 0), (0, 1), (0, 2)]).unwrap(), // K_{2, 2} (4-cycle as bipartite). - BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]), + BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]).unwrap(), // 3+3 perfect matching. no_bipartite(), // Issue's YES instance. diff --git a/src/unit_tests/rules/minimummetricdimension_ilp.rs b/src/unit_tests/rules/minimummetricdimension_ilp.rs index 431e64235..ecb741623 100644 --- a/src/unit_tests/rules/minimummetricdimension_ilp.rs +++ b/src/unit_tests/rules/minimummetricdimension_ilp.rs @@ -6,10 +6,9 @@ use crate::types::Min; #[test] fn test_minimummetricdimension_to_ilp_closed_loop() { // House graph: metric dimension = 2 - let problem = MinimumMetricDimension::new(SimpleGraph::new( - 5, - vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)], - )); + let problem = MinimumMetricDimension::new( + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(), + ); let reduction: ReductionMDToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -40,7 +39,7 @@ fn test_minimummetricdimension_to_ilp_closed_loop() { #[test] fn test_minimummetricdimension_to_ilp_structure() { // Path graph P3: 3 vertices - let problem = MinimumMetricDimension::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = MinimumMetricDimension::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction: ReductionMDToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -65,10 +64,9 @@ fn test_minimummetricdimension_to_ilp_structure() { #[test] fn test_minimummetricdimension_to_ilp_bf_vs_ilp() { // House graph - let problem = MinimumMetricDimension::new(SimpleGraph::new( - 5, - vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)], - )); + let problem = MinimumMetricDimension::new( + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]).unwrap(), + ); let reduction: ReductionMDToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); @@ -77,7 +75,8 @@ fn test_minimummetricdimension_to_ilp_bf_vs_ilp() { #[test] fn test_minimummetricdimension_to_ilp_path_graph() { // Path P4: 0-1-2-3, metric dimension = 1 (any endpoint resolves) - let problem = MinimumMetricDimension::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = + MinimumMetricDimension::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let reduction: ReductionMDToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -93,10 +92,9 @@ fn test_minimummetricdimension_to_ilp_path_graph() { #[test] fn test_minimummetricdimension_to_ilp_complete_graph() { // K4: metric dimension = 3 (n-1) - let problem = MinimumMetricDimension::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )); + let problem = MinimumMetricDimension::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), + ); let reduction: ReductionMDToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -117,7 +115,7 @@ fn test_minimummetricdimension_to_ilp_complete_graph() { #[test] fn test_minimummetricdimension_to_ilp_solution_extraction() { - let problem = MinimumMetricDimension::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = MinimumMetricDimension::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction: ReductionMDToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -133,10 +131,9 @@ fn test_minimummetricdimension_to_ilp_solution_extraction() { #[test] fn test_minimummetricdimension_to_ilp_cycle() { // C5: metric dimension = 2 - let problem = MinimumMetricDimension::new(SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], - )); + let problem = MinimumMetricDimension::new( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(), + ); let reduction: ReductionMDToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/minimummultiwaycut_ilp.rs b/src/unit_tests/rules/minimummultiwaycut_ilp.rs index 95d63b69d..e27bbb2d8 100644 --- a/src/unit_tests/rules/minimummultiwaycut_ilp.rs +++ b/src/unit_tests/rules/minimummultiwaycut_ilp.rs @@ -7,8 +7,8 @@ use crate::types::Min; /// Build the canonical 5-vertex, 3-terminal example from issue #185. fn canonical_instance() -> MinimumMultiwayCut { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); - MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]) + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]).unwrap(); + MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]).unwrap() } #[test] @@ -57,8 +57,8 @@ fn test_triangle_with_3_terminals() { // Triangle: 3 vertices, all terminals, edges: (0,1)=1, (1,2)=2, (0,2)=3 // All 3 edges must be cut to separate every terminal pair (complete graph). // Optimal cost = 1 + 2 + 3 = 6 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 1, 2], vec![1, 2, 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let problem = MinimumMultiwayCut::new(graph, vec![0, 1, 2], vec![1, 2, 3]).unwrap(); let reduction: ReductionMMCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -76,8 +76,8 @@ fn test_triangle_with_3_terminals() { fn test_two_terminals() { // Path: 0--1--2, terminals {0, 2}, weights [1, 2] // Optimal min s-t cut: cut edge (0,1) with cost 1 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1, 2]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1, 2]).unwrap(); let reduction: ReductionMMCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); diff --git a/src/unit_tests/rules/minimummultiwaycut_qubo.rs b/src/unit_tests/rules/minimummultiwaycut_qubo.rs index b300fa285..6bb18b2b0 100644 --- a/src/unit_tests/rules/minimummultiwaycut_qubo.rs +++ b/src/unit_tests/rules/minimummultiwaycut_qubo.rs @@ -4,11 +4,51 @@ use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; +#[test] +fn signed_cut_weights_preserve_every_target_optimum() { + let solver = BruteForce::new(); + for weights in [ + vec![-1, -1, -1], + vec![-3, 2, 1], + vec![0, -1, 2], + vec![i64::MIN, 0, 0], + ] { + let source = MinimumMultiwayCut::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), + vec![0, 1], + weights, + ) + .unwrap(); + let optimum = (0..8) + .filter_map(|bits| { + source + .evaluate(&(0..3).map(|i| bits & (1 << i) != 0).collect()) + .unwrap() + .0 + }) + .min() + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let solutions = solver + .find_all_witnesses(reduction.target_problem()) + .unwrap(); + assert!(!solutions.is_empty()); + for solution in solutions { + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Min(Some(optimum)) + ); + } + } +} + #[test] fn test_minimummultiwaycut_to_qubo_closed_loop() { // 5 vertices, terminals {0,2,4}, 6 edges with weights [2,3,1,2,4,5] - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); - let source = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]).unwrap(); + let source = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let qubo = reduction.target_problem(); @@ -29,8 +69,8 @@ fn test_minimummultiwaycut_to_qubo_closed_loop() { #[test] fn test_minimummultiwaycut_to_qubo_small() { // 3 vertices, 2 terminals {0,2}, edges [(0,1),(1,2)] with weights [1,1] - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let source = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1, 1]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let source = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1, 1]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let qubo = reduction.target_problem(); @@ -52,20 +92,20 @@ fn test_minimummultiwaycut_to_qubo_small() { #[test] fn test_minimummultiwaycut_to_qubo_sizes() { // 5 vertices, 3 terminals => QUBO has k*n = 3*5 = 15 variables - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); - let source = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]).unwrap(); + let source = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert_eq!(reduction.target_problem().num_variables(), 15); + assert_eq!(reduction.target_problem().num_variables().unwrap(), 15); } #[test] fn test_minimummultiwaycut_to_qubo_terminal_pinning() { // Verify that in all QUBO optimal solutions, each terminal vertex is // assigned to its own terminal position. - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]).unwrap(); let terminals = vec![0, 2, 4]; - let source = MinimumMultiwayCut::new(graph, terminals.clone(), vec![2, 3, 1, 2, 4, 5]); + let source = MinimumMultiwayCut::new(graph, terminals.clone(), vec![2, 3, 1, 2, 4, 5]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let qubo = reduction.target_problem(); diff --git a/src/unit_tests/rules/minimumsetcovering_ilp.rs b/src/unit_tests/rules/minimumsetcovering_ilp.rs index ab5a48864..f548f5ac2 100644 --- a/src/unit_tests/rules/minimumsetcovering_ilp.rs +++ b/src/unit_tests/rules/minimumsetcovering_ilp.rs @@ -6,7 +6,7 @@ use crate::types::Min; #[test] fn test_reduction_creates_valid_ilp() { // Universe: {0, 1, 2}, Sets: S0={0,1}, S1={1,2} - let problem = MinimumSetCovering::new(3, vec![vec![0, 1], vec![1, 2]]); + let problem = MinimumSetCovering::new(3, vec![vec![0, 1], vec![1, 2]]).unwrap(); let reduction: ReductionSCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -28,7 +28,8 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_reduction_weighted() { - let problem = MinimumSetCovering::with_weights(3, vec![vec![0, 1], vec![1, 2]], vec![5, 10]); + let problem = + MinimumSetCovering::with_weights(3, vec![vec![0, 1], vec![1, 2]], vec![5, 10]).unwrap(); let reduction: ReductionSCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -45,7 +46,7 @@ fn test_reduction_weighted() { fn test_minimumsetcovering_to_ilp_closed_loop() { // Universe: {0, 1, 2}, Sets: S0={0,1}, S1={1,2}, S2={0,2} // Minimum cover: any 2 sets work - let problem = MinimumSetCovering::new(3, vec![vec![0, 1], vec![1, 2], vec![0, 2]]); + let problem = MinimumSetCovering::new(3, vec![vec![0, 1], vec![1, 2], vec![0, 2]]).unwrap(); let reduction: ReductionSCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -83,7 +84,8 @@ fn test_ilp_solution_equals_brute_force_weighted() { 3, vec![vec![0, 1, 2], vec![0, 1], vec![2]], vec![10, 3, 3], - ); + ) + .unwrap(); let reduction: ReductionSCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -107,7 +109,7 @@ fn test_ilp_solution_equals_brute_force_weighted() { #[test] fn test_solution_extraction() { - let problem = MinimumSetCovering::new(4, vec![vec![0, 1], vec![2, 3]]); + let problem = MinimumSetCovering::new(4, vec![vec![0, 1], vec![2, 3]]).unwrap(); let reduction: ReductionSCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -122,7 +124,8 @@ fn test_solution_extraction() { #[test] fn test_ilp_structure() { - let problem = MinimumSetCovering::new(5, vec![vec![0, 1], vec![1, 2], vec![2, 3], vec![3, 4]]); + let problem = + MinimumSetCovering::new(5, vec![vec![0, 1], vec![1, 2], vec![2, 3], vec![3, 4]]).unwrap(); let reduction: ReductionSCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -134,7 +137,8 @@ fn test_ilp_structure() { #[test] fn test_single_set_covers_all() { // Single set covers entire universe - let problem = MinimumSetCovering::new(3, vec![vec![0, 1, 2], vec![0], vec![1], vec![2]]); + let problem = + MinimumSetCovering::new(3, vec![vec![0, 1, 2], vec![0], vec![1], vec![2]]).unwrap(); let ilp_solver = ILPSolver::new(); let reduction: ReductionSCToILP = @@ -154,7 +158,7 @@ fn test_single_set_covers_all() { #[test] fn test_overlapping_sets() { // All sets overlap on element 1 - let problem = MinimumSetCovering::new(3, vec![vec![0, 1], vec![1, 2]]); + let problem = MinimumSetCovering::new(3, vec![vec![0, 1], vec![1, 2]]).unwrap(); let ilp_solver = ILPSolver::new(); let reduction: ReductionSCToILP = @@ -174,7 +178,7 @@ fn test_overlapping_sets() { #[test] fn test_empty_universe() { // Empty universe is trivially covered - let problem = MinimumSetCovering::new(0, vec![]); + let problem = MinimumSetCovering::new(0, vec![]).unwrap(); let reduction: ReductionSCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -186,7 +190,7 @@ fn test_empty_universe() { #[test] fn test_solve_via_ilp_pipeline() { let problem: MinimumSetCovering = - MinimumSetCovering::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3], vec![0, 3]]); + MinimumSetCovering::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3], vec![0, 3]]).unwrap(); let ilp_solver = ILPSolver::new(); let solution = ilp_solver @@ -204,7 +208,7 @@ fn test_constraint_structure() { // Element 0 is in S0, S1 -> constraint: x0 + x1 >= 1 // Element 1 is in S1, S2 -> constraint: x1 + x2 >= 1 // Element 2 is in S2 -> constraint: x2 >= 1 - let problem = MinimumSetCovering::new(3, vec![vec![0], vec![0, 1], vec![1, 2]]); + let problem = MinimumSetCovering::new(3, vec![vec![0], vec![0, 1], vec![1, 2]]).unwrap(); let reduction: ReductionSCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -235,7 +239,7 @@ fn test_constraint_structure() { #[test] fn test_minimumsetcovering_to_ilp_bf_vs_ilp() { - let problem = MinimumSetCovering::new(3, vec![vec![0, 1], vec![1, 2], vec![0, 2]]); + let problem = MinimumSetCovering::new(3, vec![vec![0, 1], vec![1, 2], vec![0, 2]]).unwrap(); let reduction: ReductionSCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); diff --git a/src/unit_tests/rules/minimumsummulticenter_ilp.rs b/src/unit_tests/rules/minimumsummulticenter_ilp.rs index a6d254c93..e9cb70281 100644 --- a/src/unit_tests/rules/minimumsummulticenter_ilp.rs +++ b/src/unit_tests/rules/minimumsummulticenter_ilp.rs @@ -9,11 +9,12 @@ use crate::traits::Problem; fn test_reduction_creates_valid_ilp() { // 3-vertex path: 0 - 1 - 2, unit weights, K=1 let problem = MinimumSumMulticenter::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1i64; 3], vec![1i64; 2], 1, - ); + ) + .unwrap(); let reduction: ReductionMSMCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -34,11 +35,12 @@ fn test_minimumsummulticenter_to_ilp_bf_vs_ilp() { // 3-vertex path: 0 - 1 - 2, unit weights, K=1 // Optimal: center at vertex 1, total distance = 1+0+1 = 2 let problem = MinimumSumMulticenter::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1i64; 3], vec![1i64; 2], 1, - ); + ) + .unwrap(); let reduction: ReductionMSMCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -71,11 +73,12 @@ fn test_minimumsummulticenter_to_ilp_respects_weighted_shortest_paths() { // Triangle with a very long direct edge 0-1: // the source model must use weighted shortest paths, so center 2 is optimal. let problem = MinimumSumMulticenter::new( - SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(), vec![10i64, 10, 1], vec![100i64, 1, 1], 1, - ); + ) + .unwrap(); let bf = BruteForce::new(); let bf_witness = bf.solve(&problem).unwrap().expect("should have a solution"); @@ -104,11 +107,12 @@ fn test_minimumsummulticenter_to_ilp_respects_weighted_shortest_paths() { fn test_solution_extraction() { // 3-vertex path: center at vertex 1 let problem = MinimumSumMulticenter::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1i64; 3], vec![1i64; 2], 1, - ); + ) + .unwrap(); let reduction: ReductionMSMCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -128,7 +132,9 @@ fn test_solution_extraction() { #[test] fn test_minimumsummulticenter_to_ilp_trivial() { // Single vertex, K=1: the only vertex must be the center, distance = 0 - let problem = MinimumSumMulticenter::new(SimpleGraph::new(1, vec![]), vec![5i64], vec![], 1); + let problem = + MinimumSumMulticenter::new(SimpleGraph::new(1, vec![]).unwrap(), vec![5i64], vec![], 1) + .unwrap(); let reduction: ReductionMSMCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/minimumtardinesssequencing_ilp.rs b/src/unit_tests/rules/minimumtardinesssequencing_ilp.rs index 7f8da1782..bf3c1f2e8 100644 --- a/src/unit_tests/rules/minimumtardinesssequencing_ilp.rs +++ b/src/unit_tests/rules/minimumtardinesssequencing_ilp.rs @@ -9,7 +9,7 @@ use crate::types::One; #[test] fn test_minimumtardinesssequencing_to_ilp_closed_loop() { - let problem = MinimumTardinessSequencing::::new(3, vec![2, 3, 1], vec![(0, 2)]); + let problem = MinimumTardinessSequencing::::new(3, vec![2, 3, 1], vec![(0, 2)]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_bf_vs_ilp(&problem, &reduction); @@ -17,7 +17,8 @@ fn test_minimumtardinesssequencing_to_ilp_closed_loop() { #[test] fn test_minimumtardinesssequencing_to_ilp_bf_vs_ilp() { - let problem = MinimumTardinessSequencing::::new(4, vec![2, 3, 1, 4], vec![(0, 2)]); + let problem = + MinimumTardinessSequencing::::new(4, vec![2, 3, 1, 4], vec![(0, 2)]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf = BruteForce::new(); @@ -36,7 +37,7 @@ fn test_minimumtardinesssequencing_to_ilp_bf_vs_ilp() { #[test] fn test_minimumtardinesssequencing_to_ilp_no_precedences() { - let problem = MinimumTardinessSequencing::::new(3, vec![1, 2, 3], vec![]); + let problem = MinimumTardinessSequencing::::new(3, vec![1, 2, 3], vec![]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() @@ -48,7 +49,7 @@ fn test_minimumtardinesssequencing_to_ilp_no_precedences() { #[test] fn test_minimumtardinesssequencing_to_ilp_all_tight() { - let problem = MinimumTardinessSequencing::::new(3, vec![1, 1, 1], vec![]); + let problem = MinimumTardinessSequencing::::new(3, vec![1, 1, 1], vec![]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() @@ -65,7 +66,8 @@ fn test_minimumtardinesssequencing_to_ilp_all_tight() { #[test] fn test_minimumtardinesssequencing_weighted_to_ilp_closed_loop() { let problem = - MinimumTardinessSequencing::::with_lengths(vec![2, 1, 3], vec![3, 4, 5], vec![(0, 2)]); + MinimumTardinessSequencing::::with_lengths(vec![2, 1, 3], vec![3, 4, 5], vec![(0, 2)]) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_bf_vs_ilp(&problem, &reduction); @@ -77,7 +79,8 @@ fn test_minimumtardinesssequencing_weighted_to_ilp_vs_brute_force() { vec![3, 2, 2, 1, 2], vec![4, 3, 8, 3, 6], vec![(0, 2), (1, 3)], - ); + ) + .unwrap(); let bf = BruteForce::new(); let bf_witness = bf.solve(&problem).unwrap().expect("should have solution"); diff --git a/src/unit_tests/rules/minimumvertexcover_comparativecontainment.rs b/src/unit_tests/rules/minimumvertexcover_comparativecontainment.rs index 4786f0a13..1e2f5153d 100644 --- a/src/unit_tests/rules/minimumvertexcover_comparativecontainment.rs +++ b/src/unit_tests/rules/minimumvertexcover_comparativecontainment.rs @@ -7,7 +7,11 @@ use crate::traits::Problem; fn test_minimumvertexcover_to_comparativecontainment_closed_loop() { for bound in [1, 2] { let source = Decision::new( - MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![4i64, 2, -1]), + MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![4i64, 2, -1], + ) + .unwrap(), bound, ); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); @@ -29,9 +33,10 @@ fn test_minimumvertexcover_to_comparativecontainment_closed_loop() { } let source = Decision::new( MinimumVertexCover::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![4i64, 2, -1], - ), + ) + .unwrap(), 0, ); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); @@ -64,9 +69,10 @@ fn test_signed_containment_all_small_graphs_and_witnesses() { for bound in [-7, -2, 0, 1, 3, 9, i64::MAX] { let source = Decision::new( MinimumVertexCover::new( - SimpleGraph::new(n, edges.clone()), + SimpleGraph::new(n, edges.clone()).unwrap(), weights.clone(), - ), + ) + .unwrap(), bound, ); let reduction = @@ -87,7 +93,9 @@ fn test_signed_containment_all_small_graphs_and_witnesses() { if valid { assert_eq!(reduction.extract_solution(&witness).unwrap(), witness); } else { - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { value.is_valid() }) + ); } } } @@ -100,16 +108,19 @@ fn test_signed_containment_all_small_graphs_and_witnesses() { fn test_signed_containment_duplicate_edges_and_invalid_length() { let source = Decision::new( MinimumVertexCover::new( - SimpleGraph::new(3, vec![(0, 0), (0, 1), (0, 1)]), + SimpleGraph::new(3, vec![(0, 0), (0, 1), (0, 1)]).unwrap(), vec![-3i64, 0, 5], - ), + ) + .unwrap(), -3, ); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let witness = vec![true, false, false]; assert_eq!(reduction.extract_solution(&witness).unwrap(), witness); for bad in [vec![], vec![true; 4]] { - assert!(reduction.extract_solution(&bad).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &bad), Ok(value) if { value.is_valid() }) + ); } } @@ -127,7 +138,8 @@ fn test_signed_containment_numeric_domain() { (vec![i64::MAX / 2], 0, vec![(0, 0), (0, 0)]), ] { let source = Decision::new( - MinimumVertexCover::new(SimpleGraph::new(weights.len(), edges), weights), + MinimumVertexCover::new(SimpleGraph::new(weights.len(), edges).unwrap(), weights) + .unwrap(), bound, ); assert!(matches!( @@ -142,7 +154,7 @@ fn test_signed_containment_numeric_domain() { (0, i64::MIN + 1), ] { let source = Decision::new( - MinimumVertexCover::new(SimpleGraph::new(1, vec![]), vec![weight]), + MinimumVertexCover::new(SimpleGraph::new(1, vec![]).unwrap(), vec![weight]).unwrap(), bound, ); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); diff --git a/src/unit_tests/rules/minimumvertexcover_ensemblecomputation.rs b/src/unit_tests/rules/minimumvertexcover_ensemblecomputation.rs index b44823425..ef4253cd0 100644 --- a/src/unit_tests/rules/minimumvertexcover_ensemblecomputation.rs +++ b/src/unit_tests/rules/minimumvertexcover_ensemblecomputation.rs @@ -21,8 +21,8 @@ fn is_valid_cover(graph: &SimpleGraph, config: &[bool]) -> bool { fn test_minimumvertexcover_to_ensemblecomputation_closed_loop() { // Single edge: 2 vertices, 1 edge (0,1) // K* = 1, optimal EC length = K* + |E| = 2 - let graph = SimpleGraph::new(2, vec![(0, 1)]); - let source = MinimumVertexCover::new(graph.clone(), vec![One; 2]); + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); + let source = MinimumVertexCover::new(graph.clone(), vec![One; 2]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -56,8 +56,8 @@ fn test_minimumvertexcover_to_ensemblecomputation_closed_loop() { #[test] fn test_reduction_structure_triangle() { // Triangle K₃: 3 vertices, 3 edges - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let source = MinimumVertexCover::new(graph, vec![One; 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); + let source = MinimumVertexCover::new(graph, vec![One; 3]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -78,8 +78,8 @@ fn test_reduction_structure_triangle() { #[test] fn test_reduction_structure_path() { // Path P₃: 3 vertices {0,1,2}, 2 edges - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let source = MinimumVertexCover::new(graph, vec![One; 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let source = MinimumVertexCover::new(graph, vec![One; 3]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -92,8 +92,8 @@ fn test_reduction_structure_path() { #[test] fn test_extract_solution_correctness() { // Single edge: vertices {0,1}, edge (0,1), a₀ = 2 - let graph = SimpleGraph::new(2, vec![(0, 1)]); - let source = MinimumVertexCover::new(graph.clone(), vec![One; 2]); + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); + let source = MinimumVertexCover::new(graph.clone(), vec![One; 2]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); @@ -112,8 +112,8 @@ fn test_extract_solution_correctness() { #[test] fn test_extract_from_non_normalized_witness() { - let graph = SimpleGraph::new(2, vec![(0, 1)]); - let source = MinimumVertexCover::new(graph.clone(), vec![One; 2]); + let graph = SimpleGraph::new(2, vec![(0, 1)]).unwrap(); + let source = MinimumVertexCover::new(graph.clone(), vec![One; 2]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); @@ -130,8 +130,8 @@ fn test_extract_from_non_normalized_witness() { #[test] fn test_empty_graph() { - let graph = SimpleGraph::new(3, vec![]); - let source = MinimumVertexCover::new(graph.clone(), vec![One; 3]); + let graph = SimpleGraph::new(3, vec![]).unwrap(); + let source = MinimumVertexCover::new(graph.clone(), vec![One; 3]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -149,7 +149,7 @@ fn test_empty_graph() { #[test] fn test_minimumvertexcover_to_ensemblecomputation_zero_vertices() { - let source = MinimumVertexCover::new(SimpleGraph::new(0, vec![]), vec![]); + let source = MinimumVertexCover::new(SimpleGraph::new(0, vec![]).unwrap(), vec![]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).unwrap(); assert_eq!(reduction.target_problem().universe_size(), 1); assert_eq!(reduction.target_problem().budget(), 1); @@ -162,16 +162,23 @@ fn test_minimumvertexcover_to_ensemblecomputation_zero_vertices() { #[test] fn test_minimumvertexcover_to_ensemblecomputation_rejects_invalid_programs() { - let source = MinimumVertexCover::new(SimpleGraph::new(2, vec![(0, 1)]), vec![One; 2]); + let source = + MinimumVertexCover::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![One; 2]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).unwrap(); for program in [vec![], vec![0; 6], vec![3, 0, 1, 2, 0, 1]] { - assert!(reduction.extract_solution(&program).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &program), Ok(value) if { value.is_valid() }) + ); } } #[test] fn test_minimumvertexcover_to_ensemblecomputation_unused_and_repeated_operations() { - let source = MinimumVertexCover::new(SimpleGraph::new(5, vec![(1, 2), (1, 3)]), vec![One; 5]); + let source = MinimumVertexCover::new( + SimpleGraph::new(5, vec![(1, 2), (1, 3)]).unwrap(), + vec![One; 5], + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source).unwrap(); // Two-atom pair, useful pair, duplicate pair, unused four-atom result, // then the required triples. The final suffix is intentionally invalid. @@ -201,7 +208,9 @@ fn test_minimumvertexcover_to_ensemblecomputation_all_small_pair_families() { .enumerate() .filter_map(|(i, &edge)| (graph_mask & (1 << i) != 0).then_some(edge)) .collect(); - let source = MinimumVertexCover::new(SimpleGraph::new(4, edges.clone()), vec![One; 4]); + let source = + MinimumVertexCover::new(SimpleGraph::new(4, edges.clone()).unwrap(), vec![One; 4]) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let optimum = (0u32..16) .filter(|bits| { @@ -259,9 +268,10 @@ fn test_minimumvertexcover_to_ensemblecomputation_loops_and_parallel_edges() { // SimpleGraph's native constructor permits these representations. Duplicate // required sets need no extra operations; a loop requires its endpoint pair. let source = MinimumVertexCover::new( - SimpleGraph::new(3, vec![(0, 0), (0, 1), (1, 0), (1, 2), (1, 2)]), + SimpleGraph::new(3, vec![(0, 0), (0, 1), (1, 0), (1, 2), (1, 2)]).unwrap(), vec![One; 3], - ); + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let mut program = vec![3, 0, 3, 1, 1, 4, 2, 5]; program.resize(2 * reduction.target_problem().budget(), usize::MAX); @@ -273,7 +283,8 @@ fn test_minimumvertexcover_to_ensemblecomputation_loops_and_parallel_edges() { assert_eq!(cover, vec![true, true, false]); assert_eq!(source.evaluate(&cover).unwrap(), Min(Some(2))); - let source = MinimumVertexCover::new(SimpleGraph::new(1, vec![(0, 0)]), vec![One]); + let source = + MinimumVertexCover::new(SimpleGraph::new(1, vec![(0, 0)]).unwrap(), vec![One]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).unwrap(); assert_eq!( reduction diff --git a/src/unit_tests/rules/minimumvertexcover_ilp.rs b/src/unit_tests/rules/minimumvertexcover_ilp.rs index 371655368..d3a79494d 100644 --- a/src/unit_tests/rules/minimumvertexcover_ilp.rs +++ b/src/unit_tests/rules/minimumvertexcover_ilp.rs @@ -27,9 +27,10 @@ fn reduce_vc_to_ilp( #[test] fn test_minimumvertexcover_to_ilp_via_path_structure() { let problem = MinimumVertexCover::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); let (path, chain) = reduce_vc_to_ilp(&problem); let ilp: &ILP = chain.target_problem(); @@ -49,9 +50,10 @@ fn test_minimumvertexcover_to_ilp_via_path_structure() { #[test] fn test_minimumvertexcover_to_ilp_via_path_closed_loop() { let problem = MinimumVertexCover::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let (_, chain) = reduce_vc_to_ilp(&problem); let ilp: &ILP = chain.target_problem(); @@ -66,8 +68,11 @@ fn test_minimumvertexcover_to_ilp_via_path_closed_loop() { #[test] fn test_minimumvertexcover_to_ilp_via_path_weighted() { - let problem = - MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![100, 1, 100]); + let problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![100, 1, 100], + ) + .unwrap(); let (_, chain) = reduce_vc_to_ilp(&problem); let ilp: &ILP = chain.target_problem(); @@ -82,9 +87,10 @@ fn test_minimumvertexcover_to_ilp_via_path_weighted() { #[test] fn test_minimumvertexcover_to_ilp_bf_vs_ilp() { let problem = MinimumVertexCover::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let (_, chain) = reduce_vc_to_ilp(&problem); let ilp: &ILP = chain.target_problem(); let bf_solutions = BruteForce::new().find_all_witnesses(&problem).unwrap(); diff --git a/src/unit_tests/rules/minimumvertexcover_longestcommonsubsequence.rs b/src/unit_tests/rules/minimumvertexcover_longestcommonsubsequence.rs index 3f371a3af..6aca5e02b 100644 --- a/src/unit_tests/rules/minimumvertexcover_longestcommonsubsequence.rs +++ b/src/unit_tests/rules/minimumvertexcover_longestcommonsubsequence.rs @@ -5,9 +5,10 @@ use crate::topology::SimpleGraph; #[test] fn test_minimumvertexcover_to_longestcommonsubsequence_closed_loop() { let source = MinimumVertexCover::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![One; 4], - ); + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); @@ -21,9 +22,10 @@ fn test_minimumvertexcover_to_longestcommonsubsequence_closed_loop() { #[test] fn test_mvc_to_lcs_structure_for_path_p4() { let source = MinimumVertexCover::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![One; 4], - ); + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); @@ -47,9 +49,10 @@ fn test_mvc_to_lcs_structure_for_path_p4() { #[test] fn test_mvc_to_lcs_triangle_closed_loop() { let source = MinimumVertexCover::new( - SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(), vec![One; 3], - ); + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); @@ -62,7 +65,8 @@ fn test_mvc_to_lcs_triangle_closed_loop() { #[test] fn test_mvc_to_lcs_empty_graph_closed_loop() { - let source = MinimumVertexCover::new(SimpleGraph::new(4, vec![]), vec![One; 4]); + let source = + MinimumVertexCover::new(SimpleGraph::new(4, vec![]).unwrap(), vec![One; 4]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); @@ -82,7 +86,8 @@ fn test_mvc_to_lcs_empty_graph_closed_loop() { #[test] fn test_mvc_to_lcs_canonicalizes_edge_orientation() { - let source = MinimumVertexCover::new(SimpleGraph::new(2, vec![(1, 0)]), vec![One; 2]); + let source = + MinimumVertexCover::new(SimpleGraph::new(2, vec![(1, 0)]).unwrap(), vec![One; 2]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); diff --git a/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs b/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs index 53fd77c03..de87b7127 100644 --- a/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs +++ b/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs @@ -1,13 +1,16 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; -include!("../jl_helpers.rs"); #[test] fn test_minimumvertexcover_to_maximumindependentset_closed_loop() { // Test with weighted problems - let is_problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![10, 20, 30]); + let is_problem = MaximumIndependentSet::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![10, 20, 30], + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&is_problem) .expect("reduction should succeed"); let vc_problem = reduction.target_problem(); @@ -19,9 +22,10 @@ fn test_minimumvertexcover_to_maximumindependentset_closed_loop() { #[test] fn test_reduction_structure() { let is_problem = MaximumIndependentSet::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(), vec![1i64; 5], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&is_problem) .expect("reduction should succeed"); let vc = reduction.target_problem(); @@ -40,8 +44,11 @@ fn test_jl_parity_is_to_vertexcovering() { serde_json::from_str(include_str!("../../../tests/data/jl/independentset.json")).unwrap(); let inst = &is_data["instances"][0]["instance"]; let nv = inst["num_vertices"].as_u64().unwrap() as usize; - let source = - MaximumIndependentSet::new(SimpleGraph::new(nv, jl_parse_edges(inst)), vec![1i64; nv]); + let source = MaximumIndependentSet::new( + SimpleGraph::new(nv, jl_parse_edges(inst)).unwrap(), + vec![1i64; nv], + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let solver = BruteForce::new(); @@ -66,8 +73,11 @@ fn test_jl_parity_rule_is_to_vertexcovering() { serde_json::from_str(include_str!("../../../tests/data/jl/independentset.json")).unwrap(); let inst = &jl_find_instance_by_label(&is_data, "doc_4vertex")["instance"]; let nv = inst["num_vertices"].as_u64().unwrap() as usize; - let source = - MaximumIndependentSet::new(SimpleGraph::new(nv, jl_parse_edges(inst)), vec![1i64; nv]); + let source = MaximumIndependentSet::new( + SimpleGraph::new(nv, jl_parse_edges(inst)).unwrap(), + vec![1i64; nv], + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let solver = BruteForce::new(); diff --git a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs index ef8a2d1fb..0bb494f90 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs @@ -14,17 +14,19 @@ use crate::traits::Problem; fn triangle_source() -> MinimumVertexCover { // Triangle: 0-1-2-0, unit weights; MVC = 2 MinimumVertexCover::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(), vec![1i64; 3], ) + .unwrap() } fn weighted_path_source() -> MinimumVertexCover { // Path: 0-1-2-3-4, varied weights MinimumVertexCover::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(), vec![4, 1, 3, 2, 5], ) + .unwrap() } #[test] diff --git a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs index 9230ebe1c..ddf9bf78d 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs @@ -13,9 +13,10 @@ use crate::traits::Problem; fn weighted_cycle_cover_source() -> MinimumVertexCover { MinimumVertexCover::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 0), (2, 3), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 0), (2, 3), (3, 4)]).unwrap(), vec![4, 1, 3, 2, 5], ) + .unwrap() } #[test] diff --git a/src/unit_tests/rules/minimumvertexcover_minimumhittingset.rs b/src/unit_tests/rules/minimumvertexcover_minimumhittingset.rs index e80a1fcd3..d7de99131 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumhittingset.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumhittingset.rs @@ -20,9 +20,11 @@ fn test_minimumvertexcover_to_minimumhittingset_closed_loop() { (4, 5), (1, 4), ], - ), + ) + .unwrap(), vec![One; 6], - ); + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&vc_problem).expect("reduction should succeed"); @@ -36,8 +38,11 @@ fn test_minimumvertexcover_to_minimumhittingset_closed_loop() { #[test] fn test_vc_to_hs_structure() { // Path graph 0-1-2 with edges (0,1) and (1,2) - let vc_problem = - MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![One; 3]); + let vc_problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![One; 3], + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&vc_problem).expect("reduction should succeed"); let hs_problem = reduction.target_problem(); @@ -56,9 +61,10 @@ fn test_vc_to_hs_structure() { fn test_vc_to_hs_triangle() { // Triangle graph: 3 vertices, 3 edges let vc_problem = MinimumVertexCover::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![One; 3], - ); + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&vc_problem).expect("reduction should succeed"); let hs_problem = reduction.target_problem(); @@ -84,7 +90,8 @@ fn test_vc_to_hs_triangle() { #[test] fn test_vc_to_hs_empty_graph() { // Graph with no edges: no sets to hit - let vc_problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![]), vec![One; 3]); + let vc_problem = + MinimumVertexCover::new(SimpleGraph::new(3, vec![]).unwrap(), vec![One; 3]).unwrap(); let reduction = ReduceTo::::reduce_to(&vc_problem).expect("reduction should succeed"); let hs_problem = reduction.target_problem(); @@ -97,9 +104,10 @@ fn test_vc_to_hs_empty_graph() { fn test_vc_to_hs_star_graph() { // Star graph: center vertex 0 connected to 1, 2, 3 let vc_problem = MinimumVertexCover::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(), vec![One; 4], - ); + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&vc_problem).expect("reduction should succeed"); let hs_problem = reduction.target_problem(); @@ -123,8 +131,11 @@ fn test_vc_to_hs_star_graph() { #[test] fn test_vc_to_hs_solution_extraction() { // Verify that extract_solution is identity (1:1 correspondence) - let vc_problem = - MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![One; 3]); + let vc_problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![One; 3], + ) + .unwrap(); let reduction = ReduceTo::::reduce_to(&vc_problem).expect("reduction should succeed"); diff --git a/src/unit_tests/rules/minimumvertexcover_minimummaximalmatching.rs b/src/unit_tests/rules/minimumvertexcover_minimummaximalmatching.rs index 8677416d5..f34fc72c6 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimummaximalmatching.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimummaximalmatching.rs @@ -16,13 +16,13 @@ fn graph_from_mask(n: usize, mask: usize) -> SimpleGraph { bit += 1; } } - SimpleGraph::new(n, edges) + SimpleGraph::new(n, edges).unwrap() } #[test] fn test_minimumvertexcover_to_minimummaximalmatching_c5_gap() { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]); - let mvc = MinimumVertexCover::new(graph.clone(), vec![One; 5]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(); + let mvc = MinimumVertexCover::new(graph.clone(), vec![One; 5]).unwrap(); let mmm = MinimumMaximalMatching::new(graph); let solver = BruteForce::new(); @@ -44,7 +44,7 @@ fn test_minimumvertexcover_to_minimummaximalmatching_forward_bound_on_small_grap let num_possible_edges = n * (n.saturating_sub(1)) / 2; for mask in 0usize..(1usize << num_possible_edges) { let graph = graph_from_mask(n, mask); - let mvc = MinimumVertexCover::new(graph.clone(), vec![One; n]); + let mvc = MinimumVertexCover::new(graph.clone(), vec![One; n]).unwrap(); let mmm = MinimumMaximalMatching::new(graph); let mvc_value_solution = solver.solve(&mvc).unwrap().unwrap(); let mvc_value = mvc.evaluate(&mvc_value_solution).unwrap(); diff --git a/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs b/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs index 11accfa1f..7946f98c2 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs @@ -1,7 +1,7 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; -include!("../jl_helpers.rs"); #[test] fn test_minimumvertexcover_to_minimumsetcovering_closed_loop() { @@ -9,8 +9,11 @@ fn test_minimumvertexcover_to_minimumsetcovering_closed_loop() { // Vertex 0 covers edge 0 // Vertex 1 covers edges 0 and 1 // Vertex 2 covers edge 1 - let vc_problem = - MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i64; 3]); + let vc_problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64; 3], + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&vc_problem) .expect("reduction should succeed"); let sc_problem = reduction.target_problem(); @@ -32,9 +35,10 @@ fn test_vc_to_sc_triangle() { // Triangle graph: 3 vertices, 3 edges // Edge indices: (0,1)->0, (1,2)->1, (0,2)->2 let vc_problem = MinimumVertexCover::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&vc_problem) .expect("reduction should succeed"); let sc_problem = reduction.target_problem(); @@ -52,8 +56,11 @@ fn test_vc_to_sc_triangle() { #[test] fn test_vc_to_sc_weighted() { // Weighted problem: weights should be preserved - let vc_problem = - MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![10, 1, 10]); + let vc_problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![10, 1, 10], + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&vc_problem) .expect("reduction should succeed"); let sc_problem = reduction.target_problem(); @@ -74,7 +81,8 @@ fn test_vc_to_sc_weighted() { #[test] fn test_vc_to_sc_empty_graph() { // Graph with no edges - let vc_problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![]), vec![1i64; 3]); + let vc_problem = + MinimumVertexCover::new(SimpleGraph::new(3, vec![]).unwrap(), vec![1i64; 3]).unwrap(); let reduction = ReduceTo::>::reduce_to(&vc_problem) .expect("reduction should succeed"); let sc_problem = reduction.target_problem(); @@ -93,9 +101,10 @@ fn test_vc_to_sc_star_graph() { // Star graph: center vertex 0 connected to all others // Edges: (0,1), (0,2), (0,3) let vc_problem = MinimumVertexCover::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&vc_problem) .expect("reduction should succeed"); let sc_problem = reduction.target_problem(); @@ -124,9 +133,10 @@ fn test_jl_parity_vc_to_setcovering() { let inst = &vc_data["instances"][0]["instance"]; let nv = inst["num_vertices"].as_u64().unwrap() as usize; let source = MinimumVertexCover::new( - SimpleGraph::new(nv, jl_parse_edges(inst)), + SimpleGraph::new(nv, jl_parse_edges(inst)).unwrap(), jl_parse_i64_vec(&inst["weights"]), - ); + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let solver = BruteForce::new(); @@ -156,9 +166,10 @@ fn test_jl_parity_rule_vc_to_setcovering() { let inst = &jl_find_instance_by_label(&vc_data, "rule_4vertex")["instance"]; let nv = inst["num_vertices"].as_u64().unwrap() as usize; let source = MinimumVertexCover::new( - SimpleGraph::new(nv, jl_parse_edges(inst)), + SimpleGraph::new(nv, jl_parse_edges(inst)).unwrap(), jl_parse_i64_vec(&inst["weights"]), - ); + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let solver = BruteForce::new(); diff --git a/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs b/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs index 6b93320a5..66a0fad69 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs @@ -13,7 +13,11 @@ use crate::traits::Problem; use crate::types::Min; fn weighted_path_source() -> MinimumVertexCover { - MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![4, 1, 3]) + MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![4, 1, 3], + ) + .unwrap() } #[test] diff --git a/src/unit_tests/rules/minimumvertexcover_qubo.rs b/src/unit_tests/rules/minimumvertexcover_qubo.rs index 98c1d4935..3237c18b0 100644 --- a/src/unit_tests/rules/minimumvertexcover_qubo.rs +++ b/src/unit_tests/rules/minimumvertexcover_qubo.rs @@ -36,9 +36,10 @@ fn reduce_vc_to_qubo( #[test] fn test_minimumvertexcover_to_qubo_via_path_closed_loop() { let problem = MinimumVertexCover::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let (path, chain) = reduce_vc_to_qubo(&problem); let qubo: &QUBO = chain.target_problem(); @@ -55,7 +56,7 @@ fn test_minimumvertexcover_to_qubo_via_path_closed_loop() { "QUBO", ] ); - assert_eq!(qubo.num_variables(), 4); + assert_eq!(qubo.num_variables().unwrap(), 4); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -68,8 +69,11 @@ fn test_minimumvertexcover_to_qubo_via_path_closed_loop() { #[test] fn test_minimumvertexcover_to_qubo_via_path_weighted() { - let problem = - MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![100, 1, 100]); + let problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![100, 1, 100], + ) + .unwrap(); let (_, chain) = reduce_vc_to_qubo(&problem); let qubo: &QUBO = chain.target_problem(); @@ -87,13 +91,14 @@ fn test_minimumvertexcover_to_qubo_via_path_weighted() { #[test] fn test_minimumvertexcover_to_qubo_via_path_star_graph() { let problem = MinimumVertexCover::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let (_, chain) = reduce_vc_to_qubo(&problem); let qubo: &QUBO = chain.target_problem(); - assert_eq!(qubo.num_variables(), 4); + assert_eq!(qubo.num_variables().unwrap(), 4); let solver = BruteForce::new(); let qubo_solution = solver diff --git a/src/unit_tests/rules/minimumweightdecoding_ilp.rs b/src/unit_tests/rules/minimumweightdecoding_ilp.rs index 09eb28337..aae19b0a0 100644 --- a/src/unit_tests/rules/minimumweightdecoding_ilp.rs +++ b/src/unit_tests/rules/minimumweightdecoding_ilp.rs @@ -13,6 +13,7 @@ fn issue_instance() -> MinimumWeightDecoding { ], vec![true, true, false], ) + .unwrap() } fn small_instance() -> MinimumWeightDecoding { @@ -23,12 +24,13 @@ fn small_instance() -> MinimumWeightDecoding { vec![vec![true, true, false], vec![false, true, true]], vec![true, false], ) + .unwrap() } fn infeasible_instance() -> MinimumWeightDecoding { // H = [[1,1],[1,1]], s = [true, false] // For any x, row0 and row1 have identical dot products → s[0] ≠ s[1] means infeasible - MinimumWeightDecoding::new(vec![vec![true, true], vec![true, true]], vec![true, false]) + MinimumWeightDecoding::new(vec![vec![true, true], vec![true, true]], vec![true, false]).unwrap() } #[test] @@ -96,8 +98,9 @@ fn test_minimumweightdecoding_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionMinimumWeightDecodingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/minmaxmulticenter_ilp.rs b/src/unit_tests/rules/minmaxmulticenter_ilp.rs index 743658bfb..4f07bd7d7 100644 --- a/src/unit_tests/rules/minmaxmulticenter_ilp.rs +++ b/src/unit_tests/rules/minmaxmulticenter_ilp.rs @@ -10,11 +10,12 @@ use crate::types::Min; fn test_reduction_creates_valid_ilp() { // 3-vertex path: 0 - 1 - 2, unit weights/lengths, K=1 let problem = MinMaxMulticenter::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1i64; 3], vec![1i64; 2], 1, - ); + ) + .unwrap(); let reduction: ReductionMMCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -37,11 +38,12 @@ fn test_minmaxmulticenter_to_ilp_bf_vs_ilp() { // 3-vertex path: 0 - 1 - 2, unit weights/lengths, K=1 // Optimal: place center at vertex 1, max distance = 1 let problem = MinMaxMulticenter::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1i64; 3], vec![1i64; 2], 1, - ); + ) + .unwrap(); let reduction: ReductionMMCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -66,11 +68,12 @@ fn test_minmaxmulticenter_to_ilp_bf_vs_ilp() { fn test_solution_extraction() { // 3-vertex path: center at vertex 1 let problem = MinMaxMulticenter::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1i64; 3], vec![1i64; 2], 1, - ); + ) + .unwrap(); let reduction: ReductionMMCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -92,11 +95,12 @@ fn test_solution_extraction() { fn test_minmaxmulticenter_to_ilp_weighted() { // Single weighted edge with length 100. With k=1, optimal = 100. let problem = MinMaxMulticenter::new( - SimpleGraph::new(2, vec![(0, 1)]), + SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![1i64; 2], vec![100i64], 1, - ); + ) + .unwrap(); let bf = BruteForce::new(); let bf_witness = bf.solve(&problem).unwrap().expect("should have optimal"); @@ -115,7 +119,9 @@ fn test_minmaxmulticenter_to_ilp_weighted() { #[test] fn test_minmaxmulticenter_to_ilp_trivial() { // Single vertex, K=1: the only vertex is the center, distance = 0 - let problem = MinMaxMulticenter::new(SimpleGraph::new(1, vec![]), vec![5i64], vec![], 1); + let problem = + MinMaxMulticenter::new(SimpleGraph::new(1, vec![]).unwrap(), vec![5i64], vec![], 1) + .unwrap(); let reduction: ReductionMMCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/mixedchinesepostman_ilp.rs b/src/unit_tests/rules/mixedchinesepostman_ilp.rs index 46e5817de..48f99d5ad 100644 --- a/src/unit_tests/rules/mixedchinesepostman_ilp.rs +++ b/src/unit_tests/rules/mixedchinesepostman_ilp.rs @@ -9,7 +9,7 @@ use crate::traits::Problem; fn test_mixedchinesepostman_to_ilp_closed_loop() { // 3 vertices, 1 directed arc, 2 undirected edges let source = MixedChinesePostman::new( - MixedGraph::new(3, vec![(0, 1)], vec![(1, 2), (2, 0)]), + MixedGraph::new(3, vec![(0, 1)], vec![(1, 2), (2, 0)]).unwrap(), vec![1], vec![1, 1], ); @@ -32,7 +32,7 @@ fn test_mixedchinesepostman_to_ilp_closed_loop() { fn test_mixedchinesepostman_to_ilp_bf_vs_ilp() { // 3 vertices, 1 directed arc, 2 undirected edges let source = MixedChinesePostman::new( - MixedGraph::new(3, vec![(0, 1)], vec![(1, 2), (2, 0)]), + MixedGraph::new(3, vec![(0, 1)], vec![(1, 2), (2, 0)]).unwrap(), vec![1], vec![1, 1], ); @@ -58,7 +58,7 @@ fn test_mixedchinesepostman_to_ilp_bf_vs_ilp() { fn test_mixedchinesepostman_to_ilp_weighted() { // 3 vertices, 1 arc, 2 edges with varying weights let source = MixedChinesePostman::new( - MixedGraph::new(3, vec![(0, 1)], vec![(1, 2), (2, 0)]), + MixedGraph::new(3, vec![(0, 1)], vec![(1, 2), (2, 0)]).unwrap(), vec![2], vec![3, 1], ); @@ -87,7 +87,8 @@ fn test_mixedchinesepostman_to_ilp_with_isolated_vertices() { 8, vec![(5, 3), (1, 4), (0, 1), (2, 4), (0, 5)], vec![(4, 2), (0, 4), (0, 2), (1, 3)], - ), + ) + .unwrap(), vec![4, 5, 1, 12, 9], vec![6, 1, 13, 7], ); diff --git a/src/unit_tests/rules/monochromatictriangle_ilp.rs b/src/unit_tests/rules/monochromatictriangle_ilp.rs index 0dbd944fd..d0b001a16 100644 --- a/src/unit_tests/rules/monochromatictriangle_ilp.rs +++ b/src/unit_tests/rules/monochromatictriangle_ilp.rs @@ -6,10 +6,9 @@ use crate::topology::SimpleGraph; use crate::traits::Problem; fn k4_instance() -> MonochromaticTriangle { - MonochromaticTriangle::new(SimpleGraph::new( - 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )) + MonochromaticTriangle::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), + ) } #[test] @@ -26,7 +25,8 @@ fn test_monochromatic_triangle_to_ilp_structure() { #[test] fn test_monochromatic_triangle_to_ilp_constraint_pairs_on_single_triangle() { - let problem = MonochromaticTriangle::new(SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)])); + let problem = + MonochromaticTriangle::new(SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -66,11 +66,12 @@ fn test_monochromatic_triangle_to_ilp_infeasible_k6() { edges.push((u, v)); } } - let problem = MonochromaticTriangle::new(SimpleGraph::new(6, edges)); + let problem = MonochromaticTriangle::new(SimpleGraph::new(6, edges).unwrap()); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "K6 should be infeasible by R(3,3)=6" ); } @@ -100,7 +101,7 @@ fn test_monochromatictriangle_to_ilp_preserves_every_small_coloring() { .enumerate() .filter_map(|(i, &edge)| (mask & (1 << i) != 0).then_some(edge)) .collect(); - let source = MonochromaticTriangle::new(SimpleGraph::new(5, edges)); + let source = MonochromaticTriangle::new(SimpleGraph::new(5, edges).unwrap()); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); for bits in 0..(1 << source.num_edges()) { let coloring: Vec<_> = (0..source.num_edges()) @@ -123,7 +124,7 @@ fn test_monochromatictriangle_to_ilp_preserves_every_small_coloring() { fn test_monochromatictriangle_to_ilp_shared_k5_all_colorings() { let mut edges = vec![(0, 1), (2, 3), (4, 5), (4, 6), (5, 6)]; edges.extend((0..4).flat_map(|u| (4..7).map(move |v| (u, v)))); - let source = MonochromaticTriangle::new(SimpleGraph::new(7, edges)); + let source = MonochromaticTriangle::new(SimpleGraph::new(7, edges).unwrap()); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); assert_eq!(reduction.target_problem().num_constraints(), 49); for bits in 0..(1 << source.num_edges()) { diff --git a/src/unit_tests/rules/multiplechoicebranching_ilp.rs b/src/unit_tests/rules/multiplechoicebranching_ilp.rs index 6166a1180..c673a7363 100644 --- a/src/unit_tests/rules/multiplechoicebranching_ilp.rs +++ b/src/unit_tests/rules/multiplechoicebranching_ilp.rs @@ -5,7 +5,7 @@ use crate::traits::Problem; #[test] fn test_multiplechoicebranching_to_ilp_closed_loop() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0), (0, 2)]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0), (0, 2)]).unwrap(); for threshold in -2..=5 { let problem = MultipleChoiceBranching::new( graph.clone(), @@ -21,7 +21,10 @@ fn test_multiplechoicebranching_to_ilp_closed_loop() { let actual = reduction.extract_solution(&target).unwrap(); assert!(problem.evaluate(&actual).unwrap().0); } - None => assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()), + None => assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ), } } } @@ -29,19 +32,22 @@ fn test_multiplechoicebranching_to_ilp_closed_loop() { #[test] fn test_multiplechoicebranching_to_ilp_rejects_forced_cycle() { let problem = MultipleChoiceBranching::new( - DirectedGraph::new(2, vec![(0, 1), (1, 0)]), + DirectedGraph::new(2, vec![(0, 1), (1, 0)]).unwrap(), vec![1, 1], vec![vec![0], vec![1]], 2, ); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] fn test_multiplechoicebranching_to_ilp_size() { let problem = MultipleChoiceBranching::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2), (0, 2), (2, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2), (0, 2), (2, 2)]).unwrap(), vec![1, 2, 3, 4], vec![vec![0, 1], vec![2, 3]], 3, @@ -53,7 +59,8 @@ fn test_multiplechoicebranching_to_ilp_size() { #[test] fn test_multiplechoicebranching_to_ilp_empty_graph() { - let problem = MultipleChoiceBranching::new(DirectedGraph::new(0, vec![]), vec![], vec![], 0); + let problem = + MultipleChoiceBranching::new(DirectedGraph::new(0, vec![]).unwrap(), vec![], vec![], 0); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let target = ILPSolver::new().solve(reduction.target_problem()).unwrap(); assert_eq!( diff --git a/src/unit_tests/rules/multiplecopyfileallocation_ilp.rs b/src/unit_tests/rules/multiplecopyfileallocation_ilp.rs index 27d7b9ce5..c5537879f 100644 --- a/src/unit_tests/rules/multiplecopyfileallocation_ilp.rs +++ b/src/unit_tests/rules/multiplecopyfileallocation_ilp.rs @@ -9,10 +9,11 @@ use crate::types::Min; fn test_reduction_creates_valid_ilp() { // 3-vertex path: 0 - 1 - 2 let problem = MultipleCopyFileAllocation::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1, 1, 1], vec![5, 5, 5], - ); + ) + .unwrap(); let reduction: ReductionMCFAToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -33,10 +34,11 @@ fn test_multiplecopyfileallocation_to_ilp_bf_vs_ilp() { // storage=[5,5,5], usage=[1,1,1] // Optimal: copy at vertex 1, cost = 5 + 1 + 0 + 1 = 7 let problem = MultipleCopyFileAllocation::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1, 1, 1], vec![5, 5, 5], - ); + ) + .unwrap(); let reduction: ReductionMCFAToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -61,10 +63,11 @@ fn test_multiplecopyfileallocation_to_ilp_bf_vs_ilp() { fn test_solution_extraction() { // 3-vertex path: copy at vertex 1 (index 1 = 1) let problem = MultipleCopyFileAllocation::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1, 1, 1], vec![5, 5, 5], - ); + ) + .unwrap(); let reduction: ReductionMCFAToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -84,7 +87,9 @@ fn test_solution_extraction() { #[test] fn test_multiplecopyfileallocation_to_ilp_trivial() { // Single vertex, copy must be placed at itself, zero access cost. - let problem = MultipleCopyFileAllocation::new(SimpleGraph::new(1, vec![]), vec![2], vec![3]); + let problem = + MultipleCopyFileAllocation::new(SimpleGraph::new(1, vec![]).unwrap(), vec![2], vec![3]) + .unwrap(); let reduction: ReductionMCFAToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -103,10 +108,11 @@ fn test_multiplecopyfileallocation_to_ilp_trivial() { #[test] fn test_multiplecopyfileallocation_unreachable_assignments_are_forbidden() { let problem = MultipleCopyFileAllocation::new( - SimpleGraph::new(4, vec![(0, 1)]), + SimpleGraph::new(4, vec![(0, 1)]).unwrap(), vec![1, 0, 0, 0], vec![8, 4, 6, 2], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let direct = BruteForce::new().solve(&problem).unwrap().unwrap(); let target = ILPSolver::new().solve(reduction.target_problem()).unwrap(); diff --git a/src/unit_tests/rules/multiprocessorscheduling_ilp.rs b/src/unit_tests/rules/multiprocessorscheduling_ilp.rs index 25921170e..3b06a24c1 100644 --- a/src/unit_tests/rules/multiprocessorscheduling_ilp.rs +++ b/src/unit_tests/rules/multiprocessorscheduling_ilp.rs @@ -6,7 +6,7 @@ use crate::types::Or; #[test] fn test_reduction_creates_valid_ilp() { // 3 tasks, 2 processors, deadline 5 - let problem = MultiprocessorScheduling::new(vec![2, 3, 2], 2, 5); + let problem = MultiprocessorScheduling::new(vec![2, 3, 2], 2, 5).unwrap(); let reduction: ReductionMSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -34,7 +34,7 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_multiprocessorscheduling_to_ilp_bf_vs_ilp() { // 4 tasks [2, 2, 2, 2], 2 processors, deadline 4 → feasible (2+2 per proc) - let problem = MultiprocessorScheduling::new(vec![2, 2, 2, 2], 2, 4); + let problem = MultiprocessorScheduling::new(vec![2, 2, 2, 2], 2, 4).unwrap(); let reduction: ReductionMSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -60,7 +60,7 @@ fn test_multiprocessorscheduling_to_ilp_bf_vs_ilp() { #[test] fn test_solution_extraction() { // 3 tasks, 2 processors - let problem = MultiprocessorScheduling::new(vec![1, 2, 3], 2, 5); + let problem = MultiprocessorScheduling::new(vec![1, 2, 3], 2, 5).unwrap(); let reduction: ReductionMSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -76,7 +76,7 @@ fn test_solution_extraction() { #[test] fn test_multiprocessorscheduling_to_ilp_trivial() { // Single task on single processor - let problem = MultiprocessorScheduling::new(vec![5], 1, 5); + let problem = MultiprocessorScheduling::new(vec![5], 1, 5).unwrap(); let reduction: ReductionMSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/naesatisfiability_ilp.rs b/src/unit_tests/rules/naesatisfiability_ilp.rs index 287d57652..4e168df0c 100644 --- a/src/unit_tests/rules/naesatisfiability_ilp.rs +++ b/src/unit_tests/rules/naesatisfiability_ilp.rs @@ -80,8 +80,9 @@ fn test_naesatisfiability_to_ilp_infeasible() { let ilp_solver = ILPSolver::new(); // The ILP should be infeasible: x1 ≥ 1 (at least one true) AND x1 ≤ 0 (at least one false) - assert!( - ilp_solver.solve(ilp).is_err(), + assert_eq!( + ilp_solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible), "ILP should be infeasible for unsatisfiable NAE-SAT" ); } diff --git a/src/unit_tests/rules/naesatisfiability_maxcut.rs b/src/unit_tests/rules/naesatisfiability_maxcut.rs index 9f07e13f0..165fecb08 100644 --- a/src/unit_tests/rules/naesatisfiability_maxcut.rs +++ b/src/unit_tests/rules/naesatisfiability_maxcut.rs @@ -183,23 +183,20 @@ fn check_every_cut(source: &NAESatisfiability) { let value = target.evaluate(&cut).unwrap(); best = best.max(value.0.unwrap()); let certificate = AggregateReductionResult::extract_value(&reduction, value).0; - match reduction.extract_solution(&cut) { - Ok(assignment) => { - assert!(certificate); - assert!(source.evaluate(&assignment).unwrap().0); - assert_eq!( - assignment, - (0..source.num_vars()) - .map(|i| cut[2 * i]) - .collect::>() - ); - let index = assignment - .iter() - .enumerate() - .fold(0, |index, (i, &bit)| index | (usize::from(bit) << i)); - decoded[index] = true; - } - Err(_) => assert!(!certificate), + if certificate { + let assignment = reduction.extract_solution(&cut).unwrap(); + assert!(source.evaluate(&assignment).unwrap().0); + assert_eq!( + assignment, + (0..source.num_vars()) + .map(|i| cut[2 * i]) + .collect::>() + ); + let index = assignment + .iter() + .enumerate() + .fold(0, |index, (i, &bit)| index | (usize::from(bit) << i)); + decoded[index] = true; } } for (mask, &has_extension) in decoded.iter().enumerate() { @@ -213,9 +210,9 @@ fn check_every_cut(source: &NAESatisfiability) { decoded.iter().any(|&valid| valid) ); assert!(!AggregateReductionResult::extract_value(&reduction, crate::types::Max(None)).0); - assert!(reduction - .extract_solution(&vec![false; target.num_vertices() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vertices() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } #[test] diff --git a/src/unit_tests/rules/naesatisfiability_setsplitting.rs b/src/unit_tests/rules/naesatisfiability_setsplitting.rs index 522d9bf04..e35cfbfd7 100644 --- a/src/unit_tests/rules/naesatisfiability_setsplitting.rs +++ b/src/unit_tests/rules/naesatisfiability_setsplitting.rs @@ -54,9 +54,9 @@ fn test_naesatisfiability_to_setsplitting_extract_solution_uses_positive_literal assert_eq!( reduction - .extract_solution(&vec![true, false, true, false, true, false]) + .extract_solution(&vec![true, true, false, false, false, true]) .unwrap(), - vec![true, false, true] + vec![true, true, false] ); } diff --git a/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs b/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs index 697129347..50dc188a1 100644 --- a/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs +++ b/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs @@ -55,8 +55,9 @@ fn test_numericalmatchingwithtargetsums_to_ilp_unsatisfiable() { let problem = NumericalMatchingWithTargetSums::new(vec![1, 2], vec![3, 4], vec![10, 20]); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let result = ILPSolver::new().solve(reduction.target_problem()); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Unsatisfiable instance should have no ILP solution" ); } diff --git a/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs b/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs index a081c4de7..c0d7ac1ee 100644 --- a/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs +++ b/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs @@ -13,6 +13,7 @@ fn example_graph() -> SimpleGraph { 6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 3), (2, 5)], ) + .unwrap() } fn decision_ola(graph: SimpleGraph, k: i64) -> Decision> { @@ -87,7 +88,7 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_closed_loo #[test] fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_edgeless_sentinel() { // Edgeless graph: YES at bound zero, with one column per vertex. - let source = decision_ola(SimpleGraph::new(3, vec![]), 0); + let source = decision_ola(SimpleGraph::new(3, vec![]).unwrap(), 0); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -102,7 +103,9 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_edgeless_s let arrangement = reduction.extract_solution(&witness).unwrap(); assert_eq!(arrangement.len(), 3); assert_eq!(source.evaluate(&arrangement).unwrap(), Or(true)); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); } #[test] @@ -134,7 +137,9 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_negative_b BruteForce::new().solve(&source).unwrap().is_none(), "P_6 has no arrangement of length <= 4" ); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); } #[test] @@ -143,19 +148,13 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_extract_in let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); + assert!(reduction.target_problem().evaluate(&vec![0, 1, 2]).is_err()); assert_eq!( reduction - .extract_solution(&vec![0, 1, 2]) - .unwrap_err() - .to_string(), - "target evaluation failed during extraction: invalid configuration: column ordering length does not match the matrix" - ); - assert_eq!( - reduction - .extract_solution(&vec![0, 0, 1, 2, 3, 4]) - .unwrap_err() - .to_string(), - "target column order is not a satisfying augmentation certificate" + .target_problem() + .evaluate(&vec![0, 0, 1, 2, 3, 4]) + .unwrap(), + crate::types::Or(false) ); } @@ -172,7 +171,7 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_native_dom ]; for (n, edges, bound, expected) in cases { let m = edges.len(); - let source = decision_ola(SimpleGraph::new(n, edges), bound); + let source = decision_ola(SimpleGraph::new(n, edges).unwrap(), bound); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let target = reduction.target_problem(); assert!(target.num_rows() <= m + 3); @@ -191,19 +190,23 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_native_dom Or(true) ); } else { - assert!(reduction - .extract_solution(&(0..target.num_cols()).collect()) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &(0..target.num_cols()).collect()), Ok(value) if { value.is_valid() }) + ); } } } #[test] fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_certificate() { - let source = decision_ola(SimpleGraph::new(3, vec![(0, 2)]), 1); + let source = decision_ola(SimpleGraph::new(3, vec![(0, 2)]).unwrap(), 1); let reduction = ReduceTo::::reduce_to(&source).unwrap(); - assert!(reduction.extract_solution(&vec![0, 1, 2]).is_err()); - assert!(reduction.extract_solution(&vec![0, 1, 3]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0, 1, 2]), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0, 1, 3]), Ok(value) if { value.is_valid() }) + ); let arrangement = reduction.extract_solution(&vec![2, 0, 1]).unwrap(); assert_eq!(arrangement, vec![1, 2, 0]); assert_eq!(source.evaluate(&arrangement).unwrap(), Or(true)); diff --git a/src/unit_tests/rules/optimallineararrangement_ilp.rs b/src/unit_tests/rules/optimallineararrangement_ilp.rs index c45f0bf41..05b9e62c8 100644 --- a/src/unit_tests/rules/optimallineararrangement_ilp.rs +++ b/src/unit_tests/rules/optimallineararrangement_ilp.rs @@ -6,7 +6,8 @@ use crate::traits::Problem; #[test] fn test_reduction_creates_valid_ilp() { // Path P4: 0-1-2-3 - let problem = OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = + OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let reduction: ReductionOLAToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -18,7 +19,8 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_optimallineararrangement_to_ilp_closed_loop() { // Path graph (identity permutation achieves cost 3) - let problem = OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = + OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); // BruteForce on source to verify feasibility let bf = BruteForce::new(); let bf_solution = bf @@ -44,10 +46,13 @@ fn test_optimallineararrangement_to_ilp_closed_loop() { #[test] fn test_optimallineararrangement_to_ilp_with_chords() { // 6 vertices, path + chords - let problem = OptimalLinearArrangement::new(SimpleGraph::new( - 6, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 3), (2, 5)], - )); + let problem = OptimalLinearArrangement::new( + SimpleGraph::new( + 6, + vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 3), (2, 5)], + ) + .unwrap(), + ); // BruteForce on source let bf = BruteForce::new(); @@ -70,7 +75,8 @@ fn test_optimallineararrangement_to_ilp_with_chords() { #[test] fn test_solution_extraction() { - let problem = OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = + OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let reduction: ReductionOLAToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); @@ -83,7 +89,8 @@ fn test_solution_extraction() { #[test] fn test_optimallineararrangement_to_ilp_bf_vs_ilp() { - let problem = OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let problem = + OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap()); let reduction: ReductionOLAToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); diff --git a/src/unit_tests/rules/optimumcommunicationspanningtree_ilp.rs b/src/unit_tests/rules/optimumcommunicationspanningtree_ilp.rs index 4e0f1cf8c..431b3d33d 100644 --- a/src/unit_tests/rules/optimumcommunicationspanningtree_ilp.rs +++ b/src/unit_tests/rules/optimumcommunicationspanningtree_ilp.rs @@ -8,7 +8,7 @@ use crate::types::Min; fn k3_problem() -> OptimumCommunicationSpanningTree { let edge_weights = vec![vec![0, 1, 2], vec![1, 0, 3], vec![2, 3, 0]]; let requirements = vec![vec![0, 1, 1], vec![1, 0, 1], vec![1, 1, 0]]; - OptimumCommunicationSpanningTree::new(edge_weights, requirements) + OptimumCommunicationSpanningTree::new(edge_weights, requirements).unwrap() } fn k4_problem() -> OptimumCommunicationSpanningTree { @@ -24,7 +24,7 @@ fn k4_problem() -> OptimumCommunicationSpanningTree { vec![1, 1, 0, 2], vec![3, 1, 2, 0], ]; - OptimumCommunicationSpanningTree::new(edge_weights, requirements) + OptimumCommunicationSpanningTree::new(edge_weights, requirements).unwrap() } #[test] @@ -150,7 +150,8 @@ fn test_ocst_zero_requirement_pairs_still_enforce_spanning_tree() { vec![0, 1, 0, 4], vec![0, 4, 4, 0], ], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); let extracted = reduction.extract_solution(&solution).unwrap(); diff --git a/src/unit_tests/rules/paintshop_ilp.rs b/src/unit_tests/rules/paintshop_ilp.rs index aa721eda1..fc72db2a5 100644 --- a/src/unit_tests/rules/paintshop_ilp.rs +++ b/src/unit_tests/rules/paintshop_ilp.rs @@ -7,7 +7,7 @@ use crate::traits::Problem; #[test] fn test_reduction_creates_valid_ilp() { // Sequence: A, B, A, B => 2 cars, 4 positions - let problem = PaintShop::new(vec!["A", "B", "A", "B"]); + let problem = PaintShop::new(vec!["A", "B", "A", "B"]).unwrap(); let reduction: ReductionPaintShopToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -19,7 +19,7 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_paintshop_to_ilp_closed_loop() { - let problem = PaintShop::new(vec!["A", "B", "A", "C", "B", "C"]); + let problem = PaintShop::new(vec!["A", "B", "A", "C", "B", "C"]).unwrap(); let reduction: ReductionPaintShopToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -28,7 +28,7 @@ fn test_paintshop_to_ilp_closed_loop() { #[test] fn test_paintshop_to_ilp_bf_vs_ilp() { - let problem = PaintShop::new(vec!["A", "B", "A", "C", "B", "C"]); + let problem = PaintShop::new(vec!["A", "B", "A", "C", "B", "C"]).unwrap(); let reduction: ReductionPaintShopToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -49,7 +49,7 @@ fn test_paintshop_to_ilp_bf_vs_ilp() { #[test] fn test_solution_extraction() { // Minimal: A, A => 1 car - let problem = PaintShop::new(vec!["A", "A"]); + let problem = PaintShop::new(vec!["A", "A"]).unwrap(); let reduction: ReductionPaintShopToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); diff --git a/src/unit_tests/rules/paintshop_qubo.rs b/src/unit_tests/rules/paintshop_qubo.rs index e5b6343f2..0946102fa 100644 --- a/src/unit_tests/rules/paintshop_qubo.rs +++ b/src/unit_tests/rules/paintshop_qubo.rs @@ -5,7 +5,7 @@ use crate::solvers::BruteForce; #[test] fn test_paintshop_to_qubo_closed_loop() { // Issue example: Sequence [A, B, C, A, D, B, D, C], 4 cars - let source = PaintShop::new(vec!["A", "B", "C", "A", "D", "B", "D", "C"]); + let source = PaintShop::new(vec!["A", "B", "C", "A", "D", "B", "D", "C"]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let qubo = reduction.target_problem(); @@ -22,7 +22,7 @@ fn test_paintshop_to_qubo_closed_loop() { #[test] fn test_paintshop_to_qubo_simple() { // Simple case: a, b, a, b - let source = PaintShop::new(vec!["a", "b", "a", "b"]); + let source = PaintShop::new(vec!["a", "b", "a", "b"]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let qubo = reduction.target_problem(); @@ -38,7 +38,7 @@ fn test_paintshop_to_qubo_simple() { #[test] fn test_paintshop_to_qubo_optimal_value() { // Issue example verifies optimal QUBO = -1, total switches = -1 + 3 = 2 - let source = PaintShop::new(vec!["A", "B", "C", "A", "D", "B", "D", "C"]); + let source = PaintShop::new(vec!["A", "B", "C", "A", "D", "B", "D", "C"]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let qubo = reduction.target_problem(); @@ -56,33 +56,32 @@ fn test_paintshop_to_qubo_optimal_value() { #[test] fn test_paintshop_to_qubo_matrix_structure() { - // Issue example: verify the Q matrix matches expected values - let source = PaintShop::new(vec!["A", "B", "C", "A", "D", "B", "D", "C"]); + // Verify the Q matrix matches expected values + let source = PaintShop::new(vec!["A", "B", "C", "A", "D", "B", "D", "C"]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let qubo = reduction.target_problem(); - let m = qubo.matrix(); - // From the issue: + // Expected coefficients: // Q = [ -1, -2, 2, 2 ] // [ 0, 2, -2, 0 ] // [ 0, 0, 1, -2 ] // [ 0, 0, 0, 0 ] - assert_eq!(m[0][0], -1); - assert_eq!(m[0][1], -2); - assert_eq!(m[0][2], 2); - assert_eq!(m[0][3], 2); - assert_eq!(m[1][1], 2); - assert_eq!(m[1][2], -2); - assert_eq!(m[1][3], 0); - assert_eq!(m[2][2], 1); - assert_eq!(m[2][3], -2); - assert_eq!(m[3][3], 0); + assert_eq!(qubo.get(0, 0).unwrap(), -1); + assert_eq!(qubo.get(0, 1).unwrap(), -2); + assert_eq!(qubo.get(0, 2).unwrap(), 2); + assert_eq!(qubo.get(0, 3).unwrap(), 2); + assert_eq!(qubo.get(1, 1).unwrap(), 2); + assert_eq!(qubo.get(1, 2).unwrap(), -2); + assert_eq!(qubo.get(1, 3).unwrap(), 0); + assert_eq!(qubo.get(2, 2).unwrap(), 1); + assert_eq!(qubo.get(2, 3).unwrap(), -2); + assert_eq!(qubo.get(3, 3).unwrap(), 0); } #[test] fn test_paintshop_to_qubo_two_cars() { // Two cars, adjacent: a, b, b, a - let source = PaintShop::new(vec!["a", "b", "b", "a"]); + let source = PaintShop::new(vec!["a", "b", "b", "a"]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); assert_optimization_round_trip_from_optimization_target( @@ -95,7 +94,7 @@ fn test_paintshop_to_qubo_two_cars() { #[test] fn test_paintshop_to_qubo_empty_sequence() { // Empty PaintShop with 0 cars should not panic - let source = PaintShop::new(Vec::<&str>::new()); + let source = PaintShop::new(Vec::<&str>::new()).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let qubo = reduction.target_problem(); assert_eq!(qubo.num_vars(), 0); @@ -113,6 +112,6 @@ fn test_paintshop_to_qubo_canonical_example_spec() { assert_eq!(example.source.problem, "PaintShop"); assert_eq!(example.target.problem, "QUBO"); assert_eq!(example.source.instance["num_cars"], 4); - assert_eq!(example.target.instance["num_vars"], 4); + assert_eq!(example.target.instance["matrix"]["nrows"], 4); assert!(!example.solutions.is_empty()); } diff --git a/src/unit_tests/rules/partiallyorderedknapsack_ilp.rs b/src/unit_tests/rules/partiallyorderedknapsack_ilp.rs index f907bd954..18f218f5c 100644 --- a/src/unit_tests/rules/partiallyorderedknapsack_ilp.rs +++ b/src/unit_tests/rules/partiallyorderedknapsack_ilp.rs @@ -5,7 +5,8 @@ use crate::traits::Problem; #[test] fn test_reduction_creates_valid_ilp() { // 3 items, weights [2,3,1], values [3,4,2], capacity 4, precedence (0,1) - let problem = PartiallyOrderedKnapsack::new(vec![2, 3, 1], vec![3, 4, 2], vec![(0, 1)], 4); + let problem = + PartiallyOrderedKnapsack::new(vec![2, 3, 1], vec![3, 4, 2], vec![(0, 1)], 4).unwrap(); let reduction: ReductionPOKToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -16,7 +17,8 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_partiallyorderedknapsack_to_ilp_bf_vs_ilp() { - let problem = PartiallyOrderedKnapsack::new(vec![2, 3, 1], vec![3, 4, 2], vec![(0, 1)], 4); + let problem = + PartiallyOrderedKnapsack::new(vec![2, 3, 1], vec![3, 4, 2], vec![(0, 1)], 4).unwrap(); let reduction: ReductionPOKToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -37,7 +39,8 @@ fn test_partiallyorderedknapsack_to_ilp_bf_vs_ilp() { #[test] fn test_solution_extraction() { - let problem = PartiallyOrderedKnapsack::new(vec![2, 3, 1], vec![3, 4, 2], vec![(0, 1)], 4); + let problem = + PartiallyOrderedKnapsack::new(vec![2, 3, 1], vec![3, 4, 2], vec![(0, 1)], 4).unwrap(); let reduction: ReductionPOKToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); @@ -50,7 +53,7 @@ fn test_solution_extraction() { #[test] fn test_partiallyorderedknapsack_to_ilp_trivial() { - let problem = PartiallyOrderedKnapsack::new(vec![], vec![], vec![], 0); + let problem = PartiallyOrderedKnapsack::new(vec![], vec![], vec![], 0).unwrap(); let reduction: ReductionPOKToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/partition_integralflowwithmultipliers.rs b/src/unit_tests/rules/partition_integralflowwithmultipliers.rs index cb72e3e25..b0289975d 100644 --- a/src/unit_tests/rules/partition_integralflowwithmultipliers.rs +++ b/src/unit_tests/rules/partition_integralflowwithmultipliers.rs @@ -75,10 +75,6 @@ fn test_partition_to_integralflowwithmultipliers_odd_total_is_fixed_no_instance( assert_eq!(target.capacities(), &[1, 1]); assert_eq!(target.requirement(), 1); assert!(BruteForce::new().solve(target).unwrap().is_none()); - assert_eq!( - reduction.extract_solution(&vec![]).unwrap_err().to_string(), - "the fixed infeasible target instance has no extractable witness" - ); } #[test] diff --git a/src/unit_tests/rules/partition_openshopscheduling.rs b/src/unit_tests/rules/partition_openshopscheduling.rs index 40bc779df..3145ec02b 100644 --- a/src/unit_tests/rules/partition_openshopscheduling.rs +++ b/src/unit_tests/rules/partition_openshopscheduling.rs @@ -51,7 +51,9 @@ fn test_partition_to_open_shop_scheduling_odd_total_is_not_satisfying() { let source = Partition::new(vec![2, 4, 5]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let best = solve_target(reduction.target_problem()); - assert!(reduction.extract_solution(&best).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &best), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } #[test] @@ -122,13 +124,17 @@ fn test_partition_to_open_shop_all_small_partitions_and_machine_orders() { assert_eq!(reduction.extract_solution(&schedule).unwrap(), assignment); let delayed: Vec<_> = schedule.iter().map(|&time| time + 1).collect(); assert!(target.evaluate(&delayed).unwrap().0.is_some()); - assert!(reduction.extract_solution(&delayed).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &delayed), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } - assert!(reduction.extract_solution(&vec![0; (n + 1) * 3]).is_err()); - assert!(reduction - .extract_solution(&vec![0; (n + 1) * 3 + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; (n + 1) * 3]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; (n + 1) * 3 + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } @@ -144,7 +150,9 @@ fn test_partition_to_open_shop_odd_singleton_certificate() { .unwrap(); assert_eq!(value, crate::types::Min(Some(3))); assert!(!AggregateReductionResult::extract_value(&reduction, value).0); - assert!(reduction.extract_solution(&schedule).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &schedule), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } #[test] diff --git a/src/unit_tests/rules/partition_sequencingtominimizetardytaskweight.rs b/src/unit_tests/rules/partition_sequencingtominimizetardytaskweight.rs index 8bb870c3e..79e249fa3 100644 --- a/src/unit_tests/rules/partition_sequencingtominimizetardytaskweight.rs +++ b/src/unit_tests/rules/partition_sequencingtominimizetardytaskweight.rs @@ -65,7 +65,9 @@ fn test_partition_to_sequencing_to_minimize_tardy_task_weight_odd_total_is_unsat ) .0 ); - assert!(reduction.extract_solution(&best).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &best), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } #[cfg(feature = "example-db")] @@ -152,9 +154,8 @@ fn test_partition_to_tardy_weight_all_small_configurations() { } let certified = crate::rules::AggregateReductionResult::extract_value(&reduction, value).0; - let extracted = reduction.extract_solution(&schedule); - assert_eq!(extracted.is_ok(), certified); - if let Ok(bits) = extracted { + if certified { + let bits = reduction.extract_solution(&schedule).unwrap(); assert!(source.evaluate(&bits).unwrap().0); } } @@ -166,10 +167,12 @@ fn test_partition_to_tardy_weight_all_small_configurations() { .0, source_feasible ); - assert!(reduction.extract_solution(&vec![]).is_err()); - assert!(reduction - .extract_solution(&vec![n as usize; n as usize]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![n as usize; n as usize]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); assert!( !crate::rules::AggregateReductionResult::extract_value(&reduction, Min(None),).0 ); @@ -195,9 +198,8 @@ fn test_partition_to_tardy_weight_full_i64_domain() { crate::rules::AggregateReductionResult::extract_value(&reduction, value).0, balanced ); - let extracted = reduction.extract_solution(&schedule); - assert_eq!(extracted.is_ok(), balanced); - if let Ok(bits) = extracted { + if balanced { + let bits = reduction.extract_solution(&schedule).unwrap(); assert!(source.evaluate(&bits).unwrap().0); } } diff --git a/src/unit_tests/rules/partition_subsetsum.rs b/src/unit_tests/rules/partition_subsetsum.rs index 2e87577e8..f6104bb9a 100644 --- a/src/unit_tests/rules/partition_subsetsum.rs +++ b/src/unit_tests/rules/partition_subsetsum.rs @@ -46,12 +46,6 @@ fn test_partition_to_subsetsum_odd_total() { // No witness should exist for the target let witness = BruteForce::new().solve(target).unwrap(); assert!(witness.is_none()); - - let error = reduction.extract_solution(&vec![]).unwrap_err(); - assert_eq!( - error.to_string(), - "expected 3 subset-selection values, got 0" - ); } #[test] @@ -72,7 +66,7 @@ fn test_partition_to_subsetsum_rejects_wrong_solution_length() { let source = Partition::new(vec![1, 1, 2, 2]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); - assert!(reduction - .extract_solution(&vec![false, true, false]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false, true, false]), Ok(value) if { value.is_valid() }) + ); } diff --git a/src/unit_tests/rules/partition_sumofsquarespartition.rs b/src/unit_tests/rules/partition_sumofsquarespartition.rs index 5f5a46fc6..89bd1f915 100644 --- a/src/unit_tests/rules/partition_sumofsquarespartition.rs +++ b/src/unit_tests/rules/partition_sumofsquarespartition.rs @@ -153,5 +153,7 @@ fn test_partition_to_sumofsquarespartition_solution_extraction_identity() { ); } - assert!(reduction.extract_solution(&vec![0]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0]), Ok(value) if { value.is_valid() }) + ); } diff --git a/src/unit_tests/rules/partitionintocliques_ilp.rs b/src/unit_tests/rules/partitionintocliques_ilp.rs index 564c1af27..9a05f0232 100644 --- a/src/unit_tests/rules/partitionintocliques_ilp.rs +++ b/src/unit_tests/rules/partitionintocliques_ilp.rs @@ -5,7 +5,7 @@ use crate::types::Or; #[test] fn test_partitionintocliques_to_ilp_size() { - let problem = PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1)]), 2); + let problem = PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), 2).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); assert_eq!(reduction.target_problem().num_vars(), 6); @@ -14,7 +14,8 @@ fn test_partitionintocliques_to_ilp_size() { #[test] fn test_partitionintocliques_to_ilp_closed_loop() { - let problem = PartitionIntoCliques::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), 2); + let problem = + PartitionIntoCliques::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]).unwrap(), 2).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let target_solution = ILPSolver::new() .solve(reduction.target_problem()) @@ -26,8 +27,11 @@ fn test_partitionintocliques_to_ilp_closed_loop() { #[test] fn test_partitionintocliques_to_ilp_preserves_infeasibility() { - let problem = PartitionIntoCliques::new(SimpleGraph::new(3, vec![]), 2); + let problem = PartitionIntoCliques::new(SimpleGraph::new(3, vec![]).unwrap(), 2).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } diff --git a/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs b/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs index c3e5fb245..e5aa40a67 100644 --- a/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs +++ b/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs @@ -17,7 +17,7 @@ fn test_partitionintocliques_target_bound_rejects_overflow() { #[test] fn test_partitionintocliques_aggregate_applies_gadget_offset() { - let source = PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1)]), 2); + let source = PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), 2).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); // K + 2m + 2 = 6, including both directed-edge gadgets and the side cliques. for (value, expected) in [ @@ -35,10 +35,7 @@ fn test_partitionintocliques_aggregate_applies_gadget_offset() { #[test] fn test_partitionintocliques_to_minimumcoveringbycliques_closed_loop() { - let source: PartitionIntoCliques = serde_json::from_value(serde_json::json!({ - "graph": {"num_vertices": 0, "edges": []}, "num_cliques": 0 - })) - .unwrap(); + let source = PartitionIntoCliques::new(SimpleGraph::empty(1), 1).unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); @@ -51,7 +48,7 @@ fn test_partitionintocliques_to_minimumcoveringbycliques_closed_loop() { #[test] fn test_partitionintocliques_to_minimumcoveringbycliques_orlin_example_structure() { - let source = PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1)]), 2); + let source = PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), 2).unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -110,7 +107,7 @@ fn test_partitionintocliques_to_minimumcoveringbycliques_orlin_example_structure #[test] fn test_partitionintocliques_to_minimumcoveringbycliques_unsat_extracts_invalid_source() { - let source = PartitionIntoCliques::new(SimpleGraph::new(2, vec![]), 1); + let source = PartitionIntoCliques::new(SimpleGraph::new(2, vec![]).unwrap(), 1).unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -135,12 +132,12 @@ fn test_partitionintocliques_to_minimumcoveringbycliques_unsat_extracts_invalid_ ); assert_eq!(target.evaluate(&target_solution).unwrap(), Min(Some(4))); - assert_eq!( - reduction - .extract_solution(&target_solution) - .unwrap_err() - .to_string(), - "target cover does not certify the source clique bound" + assert!( + !crate::rules::AggregateReductionResult::extract_value( + &reduction, + target.evaluate(&target_solution).unwrap() + ) + .0 ); } @@ -154,11 +151,15 @@ fn test_partitionintocliques_native_bounds_and_adjacency_semantics() { (3, vec![(0, 1), (1, 0), (0, 0)]), ] { for bound in [0, 1, n, n + 1, usize::MAX] { - let source: PartitionIntoCliques = - serde_json::from_value(serde_json::json!({ + let source = + serde_json::from_value::>(serde_json::json!({ "graph": {"num_vertices": n, "edges": edges}, "num_cliques": bound - })) - .unwrap(); + })); + if bound == 0 || bound > n { + assert!(source.is_err()); + continue; + } + let source = source.unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = ReductionResult::target_problem(&reduction); @@ -191,12 +192,16 @@ fn test_partitionintocliques_native_bounds_and_adjacency_semantics() { assert!(source.evaluate(&decoded).unwrap().0); } } else { - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } - assert!(reduction.extract_solution(&vec![0; witness.len()]).is_err()); - assert!(reduction - .extract_solution(&vec![0; witness.len() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; witness.len()]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; witness.len() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); let q = layout.num_directed_pairs(); assert_eq!(target.num_vertices(), 2 * n + 2 * q + 4); assert_eq!(target.num_edges(), (n + q) * (n + q) + 4 * n + 7 * q + 2); diff --git a/src/unit_tests/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs b/src/unit_tests/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs index 81b97ac33..148c0a4bc 100644 --- a/src/unit_tests/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs +++ b/src/unit_tests/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs @@ -9,8 +9,10 @@ use crate::traits::Problem; #[test] fn test_partitionintopathsoflength2_to_boundedcomponentspanningforest_closed_loop() { // 6-vertex graph with two P3 paths: 0-1-2 and 3-4-5 - let source = - PartitionIntoPathsOfLength2::new(SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)])); + let source = PartitionIntoPathsOfLength2::new( + SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]).unwrap(), + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = result.target_problem(); @@ -35,10 +37,14 @@ fn test_partitionintopathsoflength2_to_boundedcomponentspanningforest_closed_loo fn test_partitionintopathsoflength2_to_boundedcomponentspanningforest_no_solution() { // 6 vertices, only edges within first 3 vertices, none in the second 3. // Second triple {3,4,5} has no edges, so it can't form a connected component. - let source = PartitionIntoPathsOfLength2::new(SimpleGraph::new( - 6, - vec![(0, 1), (1, 2), (0, 2)], // triangle on {0,1,2}, no edges on {3,4,5} - )); + let source = PartitionIntoPathsOfLength2::new( + SimpleGraph::new( + 6, + vec![(0, 1), (1, 2), (0, 2)], // triangle on {0,1,2}, no edges on {3,4,5} + ) + .unwrap(), + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let solver = BruteForce::new(); @@ -52,22 +58,26 @@ fn test_partitionintopathsoflength2_to_boundedcomponentspanningforest_no_solutio #[test] fn test_partitionintopathsoflength2_to_boundedcomponentspanningforest_triangle_partition() { // 9-vertex graph from the issue example - let source = PartitionIntoPathsOfLength2::new(SimpleGraph::new( - 9, - vec![ - (0, 1), - (1, 2), - (0, 2), - (3, 4), - (4, 5), - (6, 7), - (7, 8), - (1, 3), - (2, 6), - (5, 8), - (0, 5), - ], - )); + let source = PartitionIntoPathsOfLength2::new( + SimpleGraph::new( + 9, + vec![ + (0, 1), + (1, 2), + (0, 2), + (3, 4), + (4, 5), + (6, 7), + (7, 8), + (1, 3), + (2, 6), + (5, 8), + (0, 5), + ], + ) + .unwrap(), + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = result.target_problem(); @@ -86,8 +96,10 @@ fn test_partitionintopathsoflength2_to_boundedcomponentspanningforest_triangle_p #[test] fn test_partitionintopathsoflength2_to_boundedcomponentspanningforest_extract_solution() { // Verify extract_solution is identity - let source = - PartitionIntoPathsOfLength2::new(SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)])); + let source = PartitionIntoPathsOfLength2::new( + SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]).unwrap(), + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); diff --git a/src/unit_tests/rules/partitionintopathsoflength2_ilp.rs b/src/unit_tests/rules/partitionintopathsoflength2_ilp.rs index 35544e23d..a7273cfb6 100644 --- a/src/unit_tests/rules/partitionintopathsoflength2_ilp.rs +++ b/src/unit_tests/rules/partitionintopathsoflength2_ilp.rs @@ -7,8 +7,8 @@ use crate::types::Or; #[test] fn test_reduction_creates_valid_ilp() { // Two P3 paths: 0-1-2 and 3-4-5 - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]); - let problem = PartitionIntoPathsOfLength2::new(graph); + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]).unwrap(); + let problem = PartitionIntoPathsOfLength2::new(graph).unwrap(); let reduction: ReductionPIPL2ToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -28,8 +28,8 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_partitionintopathsoflength2_to_ilp_bf_vs_ilp() { // Two P3 paths: 0-1-2 and 3-4-5 - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]); - let problem = PartitionIntoPathsOfLength2::new(graph); + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]).unwrap(); + let problem = PartitionIntoPathsOfLength2::new(graph).unwrap(); let reduction: ReductionPIPL2ToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -55,8 +55,8 @@ fn test_partitionintopathsoflength2_to_ilp_bf_vs_ilp() { #[test] fn test_solution_extraction() { // Two P3 paths: 0-1-2 and 3-4-5 - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]); - let problem = PartitionIntoPathsOfLength2::new(graph); + let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]).unwrap(); + let problem = PartitionIntoPathsOfLength2::new(graph).unwrap(); let reduction: ReductionPIPL2ToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -77,8 +77,8 @@ fn test_solution_extraction() { #[test] fn test_partitionintopathsoflength2_to_ilp_trivial() { // Minimal feasible: one P3 path 0-1-2 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = PartitionIntoPathsOfLength2::new(graph); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = PartitionIntoPathsOfLength2::new(graph).unwrap(); let reduction: ReductionPIPL2ToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/partitionintotriangles_ilp.rs b/src/unit_tests/rules/partitionintotriangles_ilp.rs index 0173f42b3..82fd95a99 100644 --- a/src/unit_tests/rules/partitionintotriangles_ilp.rs +++ b/src/unit_tests/rules/partitionintotriangles_ilp.rs @@ -7,8 +7,8 @@ use crate::types::Or; #[test] fn test_reduction_creates_valid_ilp() { // Single triangle: 3 vertices, 3 edges, q=1 group - let graph = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); - let problem = PartitionIntoTriangles::new(graph); + let graph = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); + let problem = PartitionIntoTriangles::new(graph).unwrap(); let reduction: ReductionPITToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -28,8 +28,8 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_partitionintotriangles_to_ilp_bf_vs_ilp() { // Two triangles: vertices {0,1,2} and {3,4,5} - let graph = SimpleGraph::new(6, vec![(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5)]); - let problem = PartitionIntoTriangles::new(graph); + let graph = SimpleGraph::new(6, vec![(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5)]).unwrap(); + let problem = PartitionIntoTriangles::new(graph).unwrap(); let reduction: ReductionPITToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -55,8 +55,8 @@ fn test_partitionintotriangles_to_ilp_bf_vs_ilp() { #[test] fn test_solution_extraction() { // Two triangles: 6 vertices, q=2 groups - let graph = SimpleGraph::new(6, vec![(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5)]); - let problem = PartitionIntoTriangles::new(graph); + let graph = SimpleGraph::new(6, vec![(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5)]).unwrap(); + let problem = PartitionIntoTriangles::new(graph).unwrap(); let reduction: ReductionPITToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -71,8 +71,8 @@ fn test_solution_extraction() { #[test] fn test_partitionintotriangles_to_ilp_trivial() { // Minimal: single triangle - let graph = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); - let problem = PartitionIntoTriangles::new(graph); + let graph = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); + let problem = PartitionIntoTriangles::new(graph).unwrap(); let reduction: ReductionPITToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs b/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs index ac4111f81..50d920687 100644 --- a/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs +++ b/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs @@ -9,7 +9,7 @@ use crate::traits::Problem; fn test_pathconstrainednetworkflow_to_ilp_closed_loop() { // 3 vertices, arcs (0,1),(1,2),(0,2), caps all 1, 2 paths, req 2 let source = PathConstrainedNetworkFlow::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1, 1, 1], 0, 2, @@ -34,7 +34,7 @@ fn test_pathconstrainednetworkflow_to_ilp_closed_loop() { #[test] fn test_pathconstrainednetworkflow_to_ilp_bf_vs_ilp() { let source = PathConstrainedNetworkFlow::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1, 1, 1], 0, 2, diff --git a/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs b/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs index 3f54767c4..0b00951c0 100644 --- a/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs +++ b/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs @@ -5,12 +5,12 @@ use crate::traits::Problem; fn feasible_instance() -> PrecedenceConstrainedScheduling { // 3 tasks, 2 processors, deadline 2, precedence: task 0 must complete before task 2 - PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]) + PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]).unwrap() } fn infeasible_instance() -> PrecedenceConstrainedScheduling { // 3 tasks, 1 processor, deadline 2: impossible to fit all 3 tasks in 2 slots with 1 proc each - PrecedenceConstrainedScheduling::new(3, 1, 2, vec![]) + PrecedenceConstrainedScheduling::new(3, 1, 2, vec![]).unwrap() } #[test] @@ -60,8 +60,9 @@ fn test_precedenceconstrainedscheduling_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionPCSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible scheduling instance should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/prizecollectingsteinerforest_steinertree.rs b/src/unit_tests/rules/prizecollectingsteinerforest_steinertree.rs index 252a965c7..519d07145 100644 --- a/src/unit_tests/rules/prizecollectingsteinerforest_steinertree.rs +++ b/src/unit_tests/rules/prizecollectingsteinerforest_steinertree.rs @@ -10,7 +10,7 @@ use crate::types::Min; /// `beta * p(1) = 1` is cheaper than paying any incident edge (cost 10). fn canonical_problem() -> PrizeCollectingSteinerForest { PrizeCollectingSteinerForest::::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![5, 1, 5], vec![10, 10], 1, @@ -33,14 +33,14 @@ fn test_prizecollectingsteinerforest_to_steinertree_canonical_closed_loop() { "PCSF -> SteinerTree canonical closed loop", ); - // Numeric sanity: both optima must agree, and equal 3 on this instance. + // Three gadget terminals each add M = omega + 1 = 2. let target = reduction.target_problem(); let source_opt_solution = BruteForce::new().solve(&source).unwrap().unwrap(); let source_opt = source.evaluate(&source_opt_solution).unwrap(); let target_opt_solution = BruteForce::new().solve(target).unwrap().unwrap(); let target_opt = target.evaluate(&target_opt_solution).unwrap(); assert_eq!(source_opt, Min(Some(3))); - assert_eq!(target_opt, Min(Some(3))); + assert_eq!(target_opt, Min(Some(9))); } #[test] @@ -98,7 +98,7 @@ fn test_prizecollectingsteinerforest_to_steinertree_extract_witness_canonical() #[test] fn test_prizecollectingsteinerforest_to_steinertree_all_prizes() { let source = PrizeCollectingSteinerForest::::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), // Large prizes so all three vertices are worth including. vec![100, 100, 100], vec![1, 1], @@ -128,22 +128,14 @@ fn test_prizecollectingsteinerforest_to_steinertree_all_prizes() { let target_opt_solution = BruteForce::new().solve(target).unwrap().unwrap(); let target_opt = target.evaluate(&target_opt_solution).unwrap(); assert_eq!(source_opt, Min(Some(3))); - assert_eq!(target_opt, Min(Some(3))); + assert_eq!(target_opt, Min(Some(9))); } -/// No vertex carries a positive prize, so no gadget terminals are added. -/// Only the artificial root remains as a terminal, but SteinerTree requires -/// at least two terminals — so this corner case is covered by size-contract -/// inspection plus a degenerate single-vertex source case that still has -/// the construction proceed when `omega = 0`. We skip the SteinerTree -/// instantiation when `k = 0` (which would produce a single-terminal -/// SteinerTree); the closed-loop check uses a near-empty case where one -/// vertex has prize 0 and one has a positive prize. #[test] fn test_prizecollectingsteinerforest_to_steinertree_mixed_zero_prize() { // Two-vertex path with one prize-zero vertex. let source = PrizeCollectingSteinerForest::::new( - SimpleGraph::new(2, vec![(0, 1)]), + SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![0, 5], vec![1], 1, @@ -174,7 +166,7 @@ fn test_prizecollectingsteinerforest_to_steinertree_path_with_omission() { // beta = 1, omega = 1. Vertices 1 and 2 are expected to drop because // each edge costs 5 but their prize is only 1. let source = PrizeCollectingSteinerForest::::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![4, 1, 1, 4], vec![5, 5, 5], 1, @@ -189,3 +181,96 @@ fn test_prizecollectingsteinerforest_to_steinertree_path_with_omission() { "PCSF -> SteinerTree path-with-omission case", ); } + +#[test] +fn test_zero_prize_forest_through_steiner_tree_and_ilp() { + use crate::models::algebraic::ILP; + use crate::solvers::ILPSolver; + for (n, edges, costs, expected) in [(0, vec![], vec![], 0), (2, vec![(0, 1)], vec![5], 0)] { + let source = PrizeCollectingSteinerForest::new( + SimpleGraph::new(n, edges).unwrap(), + vec![0; n], + costs, + 1, + 1, + ) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!(reduction.target_problem().num_terminals(), 1); + let ilp = ReduceTo::>::reduce_to(reduction.target_problem()).unwrap(); + let raw = ILPSolver::new().solve(ilp.target_problem()).unwrap(); + let tree = ilp.extract_solution(&raw).unwrap(); + let forest = reduction.extract_solution(&tree).unwrap(); + assert_eq!(source.evaluate(&forest).unwrap(), Min(Some(expected))); + assert_optimization_round_trip_from_optimization_target( + &source, + &reduction, + "zero-prize forest", + ); + } +} + +#[test] +fn low_prizes_do_not_bypass_component_costs() { + for (prizes, beta, omega, expected) in [ + (vec![1, 2], 1, 5, 3), + (vec![1, 2], 0, 5, 0), + (vec![1, 2], 1, 0, 0), + (vec![0, 2], 1, 5, 2), + ] { + let source = PrizeCollectingSteinerForest::new( + SimpleGraph::new(2, vec![(0, 1)]).unwrap(), + prizes, + vec![0], + beta, + omega, + ) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let offset = (omega + 1) * source.num_vertices_with_prize() as i64; + let trees = BruteForce::new() + .find_all_witnesses(reduction.target_problem()) + .unwrap(); + assert!(!trees.is_empty()); + for tree in trees { + assert_eq!( + reduction.target_problem().evaluate(&tree).unwrap(), + Min(Some(expected + offset)) + ); + let forest = reduction.extract_solution(&tree).unwrap(); + assert_eq!(source.evaluate(&forest).unwrap(), Min(Some(expected))); + } + } +} + +#[test] +fn gadget_coefficients_report_native_integer_overflow() { + for (prize, beta, omega) in [(1, 1, i64::MAX), (i64::MAX, 2, 0), (i64::MAX, 1, 0)] { + let source = PrizeCollectingSteinerForest::new( + SimpleGraph::new(1, vec![]).unwrap(), + vec![prize], + vec![], + beta, + omega, + ) + .unwrap(); + assert!(matches!( + ReduceTo::>::reduce_to(&source), + Err(crate::rules::ReductionError::IntegerOverflow { .. }) + )); + } + // No prize gadget is constructed, so its inclusion cost is not needed. + let source = PrizeCollectingSteinerForest::new( + SimpleGraph::new(1, vec![]).unwrap(), + vec![0], + vec![], + 1, + i64::MAX, + ) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!( + reduction.target_problem().evaluate(&vec![false]).unwrap(), + Min(Some(0)) + ); +} diff --git a/src/unit_tests/rules/quadraticassignment_ilp.rs b/src/unit_tests/rules/quadraticassignment_ilp.rs index c68fd73e1..b148711cb 100644 --- a/src/unit_tests/rules/quadraticassignment_ilp.rs +++ b/src/unit_tests/rules/quadraticassignment_ilp.rs @@ -7,6 +7,7 @@ fn small_qap() -> QuadraticAssignment { vec![vec![0, 5, 2], vec![5, 0, 3], vec![2, 3, 0]], vec![vec![0, 4, 1], vec![4, 0, 3], vec![1, 3, 0]], ) + .unwrap() } #[test] @@ -51,7 +52,8 @@ fn test_quadraticassignment_to_ilp_closed_loop() { #[test] fn test_quadraticassignment_to_ilp_2x2() { let problem = - QuadraticAssignment::new(vec![vec![0, 1], vec![1, 0]], vec![vec![0, 2], vec![2, 0]]); + QuadraticAssignment::new(vec![vec![0, 1], vec![1, 0]], vec![vec![0, 2], vec![2, 0]]) + .unwrap(); // BruteForce on source let bf = BruteForce::new(); let bf_solution = bf.solve(&problem).unwrap().expect("brute-force optimum"); @@ -91,7 +93,8 @@ fn test_quadraticassignment_to_ilp_rectangular() { let problem = QuadraticAssignment::new( vec![vec![0, 3], vec![3, 0]], vec![vec![0, 1, 5], vec![1, 0, 2], vec![5, 2, 0]], - ); + ) + .unwrap(); // BruteForce on source let bf = BruteForce::new(); let bf_solution = bf.solve(&problem).unwrap().expect("brute-force optimum"); diff --git a/src/unit_tests/rules/qubo_casts.rs b/src/unit_tests/rules/qubo_casts.rs index 9ac1eb48a..002a36469 100644 --- a/src/unit_tests/rules/qubo_casts.rs +++ b/src/unit_tests/rules/qubo_casts.rs @@ -9,7 +9,9 @@ fn test_qubo_i64_to_f64_closed_loop() { assert_eq!( reduction.target_problem().matrix(), - &[vec![1.0, -2.0], vec![0.0, 3.0]] + QUBO::from_matrix(vec![vec![1.0, -2.0], vec![0.0, 3.0]]) + .unwrap() + .matrix() ); assert_eq!( reduction.extract_solution(&vec![true, false]).unwrap(), diff --git a/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs b/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs index 056c224de..a3f5cbcd6 100644 --- a/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs +++ b/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs @@ -5,7 +5,8 @@ use crate::types::Or; #[test] fn test_reduction_creates_valid_ilp() { - let problem = RectilinearPictureCompression::new(vec![vec![true, true], vec![true, false]], 2); + let problem = + RectilinearPictureCompression::new(vec![vec![true, true], vec![true, false]], 2).unwrap(); let reduction: ReductionRPCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -16,7 +17,8 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_rectilinearpicturecompression_to_ilp_bf_vs_ilp() { - let problem = RectilinearPictureCompression::new(vec![vec![true, true], vec![true, true]], 1); + let problem = + RectilinearPictureCompression::new(vec![vec![true, true], vec![true, true]], 1).unwrap(); let reduction: ReductionRPCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -34,7 +36,8 @@ fn test_rectilinearpicturecompression_to_ilp_bf_vs_ilp() { #[test] fn test_solution_extraction() { - let problem = RectilinearPictureCompression::new(vec![vec![true, true], vec![true, true]], 2); + let problem = + RectilinearPictureCompression::new(vec![vec![true, true], vec![true, true]], 2).unwrap(); let reduction: ReductionRPCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); @@ -49,7 +52,8 @@ fn test_solution_extraction() { fn test_rectilinearpicturecompression_to_ilp_trivial() { // All-zero matrix: no 1-cells, trivially feasible let problem = - RectilinearPictureCompression::new(vec![vec![false, false], vec![false, false]], 0); + RectilinearPictureCompression::new(vec![vec![false, false], vec![false, false]], 0) + .unwrap(); let reduction: ReductionRPCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/reduction_path_parity.rs b/src/unit_tests/rules/reduction_path_parity.rs index d2011bcd4..b1273f227 100644 --- a/src/unit_tests/rules/reduction_path_parity.rs +++ b/src/unit_tests/rules/reduction_path_parity.rs @@ -43,7 +43,8 @@ fn test_jl_parity_maxcut_to_spinglass_path() { (6, 9), (7, 9), ]; - let source = MaxCut::::unweighted(SimpleGraph::new(10, petersen_edges)); + let source = + MaxCut::::unweighted(SimpleGraph::new(10, petersen_edges).unwrap()); let chain = graph .reduce_along_path(&rpath, &source as &dyn std::any::Any) .expect("MaxCut -> SpinGlass reduction should not fail") @@ -93,7 +94,8 @@ fn test_jl_parity_maxcut_to_qubo_path() { (6, 9), (7, 9), ]; - let source = MaxCut::::unweighted(SimpleGraph::new(10, petersen_edges)); + let source = + MaxCut::::unweighted(SimpleGraph::new(10, petersen_edges).unwrap()); let chain = graph .reduce_along_path(&rpath, &source as &dyn std::any::Any) .expect("MaxCut -> QUBO reduction should not fail") @@ -131,7 +133,7 @@ fn test_jl_parity_factoring_to_spinglass_path() { // Verify reduction produces a valid SpinGlass problem assert!( - target.num_variables() > 0, + target.num_variables().unwrap() > 0, "SpinGlass should have variables" ); diff --git a/src/unit_tests/rules/registersufficiency_ilp.rs b/src/unit_tests/rules/registersufficiency_ilp.rs index beaef71d7..7e67bb8d8 100644 --- a/src/unit_tests/rules/registersufficiency_ilp.rs +++ b/src/unit_tests/rules/registersufficiency_ilp.rs @@ -5,11 +5,11 @@ use crate::traits::Problem; use crate::types::Or; fn feasible_example() -> RegisterSufficiency { - RegisterSufficiency::new(4, vec![(2, 0), (3, 1)], 2) + RegisterSufficiency::new(4, vec![(2, 0), (3, 1)], 2).unwrap() } fn infeasible_example() -> RegisterSufficiency { - RegisterSufficiency::new(4, vec![(1, 0), (2, 1), (3, 2), (3, 0)], 1) + RegisterSufficiency::new(4, vec![(1, 0), (2, 1), (3, 2), (3, 0)], 1).unwrap() } #[allow(dead_code)] @@ -28,6 +28,7 @@ fn canonical_example() -> RegisterSufficiency { ], 3, ) + .unwrap() } #[test] @@ -63,8 +64,9 @@ fn test_register_sufficiency_to_ilp_infeasible() { let source = infeasible_example(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "register-sufficiency instance with bound one should be infeasible" ); } diff --git a/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs b/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs index 9e811bbd4..eb2438fb6 100644 --- a/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs +++ b/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs @@ -50,8 +50,9 @@ fn test_resourceconstrainedscheduling_to_ilp_infeasible() { let problem = ResourceConstrainedScheduling::new(1, vec![5], vec![vec![6], vec![6], vec![6]], 1).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible RCS should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/rootedtreearrangement_rootedtreestorageassignment.rs b/src/unit_tests/rules/rootedtreearrangement_rootedtreestorageassignment.rs index da6346190..81899913b 100644 --- a/src/unit_tests/rules/rootedtreearrangement_rootedtreestorageassignment.rs +++ b/src/unit_tests/rules/rootedtreearrangement_rootedtreestorageassignment.rs @@ -6,7 +6,10 @@ use crate::solvers::BruteForce; fn test_rootedtreearrangement_to_rootedtreestorageassignment_closed_loop() { // Path graph P4: 0-1-2-3, bound K=5 // Optimal chain tree gives total distance 3 <= 5 - let source = RootedTreeArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), 5); + let source = RootedTreeArrangement::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), + 5, + ); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); assert_satisfaction_round_trip_from_satisfaction_target(&source, &reduction, "P4 path graph"); @@ -15,7 +18,10 @@ fn test_rootedtreearrangement_to_rootedtreestorageassignment_closed_loop() { #[test] fn test_rootedtreearrangement_to_rootedtreestorageassignment_target_structure() { // Triangle graph: 3 vertices, 3 edges, bound K=6 - let source = RootedTreeArrangement::new(SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]), 6); + let source = RootedTreeArrangement::new( + SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(), + 6, + ); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -36,7 +42,10 @@ fn test_rootedtreearrangement_to_rootedtreestorageassignment_target_structure() fn test_rootedtreearrangement_to_rootedtreestorageassignment_star_graph() { // Star graph K_{1,3}: center=0, leaves=1,2,3 // Bound K=3 (optimal: root at 0, each leaf distance 1, total=3) - let source = RootedTreeArrangement::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]), 3); + let source = RootedTreeArrangement::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(), + 3, + ); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -52,7 +61,7 @@ fn test_rootedtreearrangement_to_rootedtreestorageassignment_unsatisfiable() { // root-to-leaf paths. K4 has 6 edges, and its minimum total stretch // on a chain tree is 1+1+1+2+2+3=10. With K=7 it should be infeasible. let source = RootedTreeArrangement::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), 7, ); let reduction = ReduceTo::::reduce_to(&source) @@ -77,7 +86,7 @@ fn test_rootedtreearrangement_to_rootedtreestorageassignment_unsatisfiable() { #[test] fn test_rootedtreearrangement_to_rootedtreestorageassignment_solution_extraction() { // Simple edge: 2 vertices, 1 edge {0,1}, bound K=1 - let source = RootedTreeArrangement::new(SimpleGraph::new(2, vec![(0, 1)]), 1); + let source = RootedTreeArrangement::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), 1); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -99,7 +108,7 @@ fn test_rootedtreearrangement_to_rootedtreestorageassignment_solution_extraction #[test] fn test_rootedtreearrangement_to_rootedtreestorageassignment_empty_graph() { // Graph with no edges - let source = RootedTreeArrangement::new(SimpleGraph::new(3, vec![]), 0); + let source = RootedTreeArrangement::new(SimpleGraph::new(3, vec![]).unwrap(), 0); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -115,7 +124,10 @@ fn test_rootedtreearrangement_to_rootedtreestorageassignment_empty_graph() { fn test_rootedtreearrangement_to_rootedtreestorageassignment_infeasible_underflow() { // K < |E|: bound is too small for a 3-edge path, so source is infeasible. // The reduction should return an infeasible gadget rather than panic. - let source = RootedTreeArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), 2); + let source = RootedTreeArrangement::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), + 2, + ); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs b/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs index 737aec834..a19c78a93 100644 --- a/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs +++ b/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs @@ -42,9 +42,10 @@ fn test_rootedtreestorageassignment_to_ilp_bf_vs_ilp() { assert!(ilp_value.0, "ILP solution should be feasible"); assert!(bf_value.0, "BF should also find feasible solution"); } - Err(_) => { + Err(crate::solvers::ILPSolveError::Infeasible) => { assert!(!bf_value.0, "both should agree on infeasibility"); } + Err(error) => panic!("ILP execution failed: {error}"), } } @@ -66,7 +67,11 @@ fn test_rootedtreestorageassignment_to_ilp_infeasible() { let ilp_solver = ILPSolver::new(); let ilp_result = ilp_solver.solve(reduction.target_problem()); assert!(bf_witness.is_none(), "source should be infeasible"); - assert!(ilp_result.is_err(), "reduced ILP should also be infeasible"); + assert_eq!( + ilp_result, + Err(crate::solvers::ILPSolveError::Infeasible), + "reduced ILP should also be infeasible" + ); } #[test] diff --git a/src/unit_tests/rules/ruralpostman_ilp.rs b/src/unit_tests/rules/ruralpostman_ilp.rs index ac981b878..76402825c 100644 --- a/src/unit_tests/rules/ruralpostman_ilp.rs +++ b/src/unit_tests/rules/ruralpostman_ilp.rs @@ -9,10 +9,11 @@ use crate::traits::Problem; fn test_ruralpostman_to_ilp_closed_loop() { // Triangle: 3 vertices, 3 edges, require edge 0 let source = RuralPostman::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1, 1, 1], vec![0], - ); + ) + .unwrap(); let direct = BruteForce::new() .solve(&source) .unwrap() @@ -32,10 +33,11 @@ fn test_ruralpostman_to_ilp_closed_loop() { fn test_ruralpostman_to_ilp_optimization() { // Triangle with varied weights: require edges 0 and 1 let source = RuralPostman::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![2, 3, 1], vec![0, 1], - ); + ) + .unwrap(); // Brute-force optimal on the source let bf_witness = BruteForce::new() @@ -62,10 +64,11 @@ fn test_ruralpostman_to_ilp_optimization() { #[test] fn test_ruralpostman_to_ilp_bf_vs_ilp() { let source = RuralPostman::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1, 1, 1], vec![0], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } @@ -73,10 +76,11 @@ fn test_ruralpostman_to_ilp_bf_vs_ilp() { #[test] fn test_ruralpostman_empty_required_set_extracts_zero_multiplicities() { let source = RuralPostman::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![4, 7], vec![], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = ILPSolver::new().solve(reduction.target_problem()).unwrap(); let extracted = reduction.extract_solution(&target).unwrap(); diff --git a/src/unit_tests/rules/sat_coloring.rs b/src/unit_tests/rules/sat_coloring.rs index 42fc5e598..bae7be66f 100644 --- a/src/unit_tests/rules/sat_coloring.rs +++ b/src/unit_tests/rules/sat_coloring.rs @@ -1,10 +1,10 @@ use super::*; +include!("../jl_helpers.rs"); use crate::models::formula::CNFClause; use crate::solvers::BruteForce; use crate::topology::Graph; use crate::traits::Problem; use crate::variant::K3; -include!("../jl_helpers.rs"); #[test] fn test_constructor_basic_structure() { diff --git a/src/unit_tests/rules/sat_ksat.rs b/src/unit_tests/rules/sat_ksat.rs index 4e4cfa7c5..e7b3bf744 100644 --- a/src/unit_tests/rules/sat_ksat.rs +++ b/src/unit_tests/rules/sat_ksat.rs @@ -1,9 +1,9 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; use crate::traits::Problem; use crate::variant::K3; -include!("../jl_helpers.rs"); #[test] fn test_sat_to_3sat_exact_size() { diff --git a/src/unit_tests/rules/sat_maximumindependentset.rs b/src/unit_tests/rules/sat_maximumindependentset.rs index 47b23dd0d..f4fb3b2c5 100644 --- a/src/unit_tests/rules/sat_maximumindependentset.rs +++ b/src/unit_tests/rules/sat_maximumindependentset.rs @@ -1,10 +1,10 @@ use super::*; +include!("../jl_helpers.rs"); use crate::models::formula::CNFClause; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; use crate::solvers::BruteForce; use crate::topology::Graph; use crate::traits::Problem; -include!("../jl_helpers.rs"); #[test] fn test_boolvar_creation() { @@ -230,7 +230,9 @@ fn test_jl_parity_sat_to_independentset() { .solve(result.target_problem()) .unwrap() .expect("SAT->IS: target should have an optimal solution"); - assert!(result.extract_solution(&target_solution).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&result), &target_solution), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&result, value); value.is_valid() }) + ); assert_eq!( crate::rules::AggregateReductionResult::extract_value( &result, @@ -292,22 +294,19 @@ fn test_sat_to_independentset_all_certificates() { crate::rules::AggregateReductionResult::extract_value(&reduction, value), Or(certificate) ); - match reduction.extract_solution(&config) { - Ok(assignment) => { - assert!(certificate); - assert_eq!(source.evaluate(&assignment).unwrap(), Or(true)); - accepted = true; - } - Err(_) => assert!(!certificate), + if certificate { + let assignment = reduction.extract_solution(&config).unwrap(); + assert_eq!(source.evaluate(&assignment).unwrap(), Or(true)); + accepted = true; } } assert_eq!( accepted, BruteForce::new().solve(&source).unwrap().is_some() ); - assert!(reduction - .extract_solution(&vec![false; target.num_vertices() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vertices() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } for num_vars in [0, 3] { diff --git a/src/unit_tests/rules/sat_minimumdominatingset.rs b/src/unit_tests/rules/sat_minimumdominatingset.rs index c6db9615d..21c6331a0 100644 --- a/src/unit_tests/rules/sat_minimumdominatingset.rs +++ b/src/unit_tests/rules/sat_minimumdominatingset.rs @@ -1,9 +1,10 @@ use super::*; +use crate::traits::Problem; +include!("../jl_helpers.rs"); use crate::models::formula::CNFClause; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; use crate::solvers::BruteForce; use crate::topology::Graph; -include!("../jl_helpers.rs"); #[test] fn test_sat_to_minimumdominatingset_closed_loop() { @@ -144,9 +145,12 @@ fn test_extract_solution_too_many_selected() { .expect("reduction should succeed"); let ds_sol = vec![true, true, false, false]; - assert_eq!( - reduction.extract_solution(&ds_sol).unwrap_err().to_string(), - "target dominating set does not certify satisfiability" + assert!( + !crate::rules::AggregateReductionResult::extract_value( + &reduction, + reduction.target_problem().evaluate(&ds_sol).unwrap() + ) + .0 ); } @@ -156,12 +160,15 @@ fn test_extract_solution_rejects_unselected_variable_gadget() { let reduction = ReduceTo::>::reduce_to(&sat) .expect("reduction should succeed"); - assert_eq!( - reduction - .extract_solution(&vec![false, false, false, false]) - .unwrap_err() - .to_string(), - "target dominating set does not certify satisfiability" + assert!( + !crate::rules::AggregateReductionResult::extract_value( + &reduction, + reduction + .target_problem() + .evaluate(&vec![false, false, false, false]) + .unwrap() + ) + .0 ); } @@ -171,12 +178,15 @@ fn test_extract_solution_rejects_selected_clause_vertex() { let reduction = ReduceTo::>::reduce_to(&sat) .expect("reduction should succeed"); - assert_eq!( - reduction - .extract_solution(&vec![true, false, false, true]) - .unwrap_err() - .to_string(), - "target dominating set does not certify satisfiability" + assert!( + !crate::rules::AggregateReductionResult::extract_value( + &reduction, + reduction + .target_problem() + .evaluate(&vec![true, false, false, true]) + .unwrap() + ) + .0 ); } @@ -247,7 +257,9 @@ fn test_jl_parity_sat_to_dominatingset() { .solve(result.target_problem()) .unwrap() .expect("SAT->DS: target should have an optimal solution"); - assert!(result.extract_solution(&target_solution).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&result), &target_solution), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&result, value); value.is_valid() }) + ); } else { assert_satisfaction_round_trip_from_optimization_target( &source, @@ -296,22 +308,19 @@ fn test_sat_to_dominatingset_native_certificates() { crate::rules::AggregateReductionResult::extract_value(&result, value), Or(certificate) ); - match result.extract_solution(&config) { - Ok(x) => { - assert!(certificate); - assert_eq!(source.evaluate(&x).unwrap(), Or(true)); - accepted = true; - } - Err(_) => assert!(!certificate), + if certificate { + let x = result.extract_solution(&config).unwrap(); + assert_eq!(source.evaluate(&x).unwrap(), Or(true)); + accepted = true; } } assert_eq!( accepted, BruteForce::new().solve(&source).unwrap().is_some() ); - assert!(result - .extract_solution(&vec![false; target.num_vertices() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&result), &vec![false; target.num_vertices() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&result, value); value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/satisfiability_maximum2satisfiability.rs b/src/unit_tests/rules/satisfiability_maximum2satisfiability.rs index 3db2ebc6d..d5ed64c13 100644 --- a/src/unit_tests/rules/satisfiability_maximum2satisfiability.rs +++ b/src/unit_tests/rules/satisfiability_maximum2satisfiability.rs @@ -73,7 +73,9 @@ fn test_satisfiability_to_maximum2satisfiability_unsatisfiable_gap() { .solve(target) .unwrap() .expect("MAX-2-SAT target should always have a witness"); - assert!(reduction.extract_solution(&target_solution).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &target_solution), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); assert_eq!( crate::rules::AggregateReductionResult::extract_value(&reduction, Max(Some(55))), Or(false) @@ -162,14 +164,16 @@ fn test_satisfiability_to_maximum2satisfiability_every_target_witness() { let decoded = reduction.extract_solution(&assignment).unwrap(); assert!(source.evaluate(&decoded).unwrap().0); } else { - assert!(reduction.extract_solution(&assignment).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &assignment), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } let source_yes = BruteForce::new().solve(&source).unwrap().is_some(); assert_eq!(best == threshold, source_yes); - assert!(reduction - .extract_solution(&vec![false; target.num_vars() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vars() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); assert_eq!( crate::rules::AggregateReductionResult::extract_value(&reduction, Max(None)), Or(false) diff --git a/src/unit_tests/rules/satisfiability_naesatisfiability.rs b/src/unit_tests/rules/satisfiability_naesatisfiability.rs index e1ee71120..a88b6fcb4 100644 --- a/src/unit_tests/rules/satisfiability_naesatisfiability.rs +++ b/src/unit_tests/rules/satisfiability_naesatisfiability.rs @@ -86,14 +86,13 @@ fn test_solution_extraction_distinguishes_zero_assignment_from_malformed_input() vec![false, false] ); - let error = reduction.extract_solution(&vec![false, false]).unwrap_err(); - assert_eq!( - error.to_string(), - "target evaluation failed during extraction: invalid configuration: assignment length does not match the formula variables" - ); assert!(reduction - .extract_solution(&vec![false, false, false, false]) + .target_problem() + .evaluate(&vec![false, false]) .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false, false, false, false]), Ok(value) if { value.is_valid() }) + ); assert!(crate::rules::DynReductionResult::target_solution_from_json( &reduction, serde_json::json!([false, 2, false]) diff --git a/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs b/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs index 8838a3843..f0ff87112 100644 --- a/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs +++ b/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs @@ -5,12 +5,12 @@ use crate::traits::Problem; fn feasible_instance() -> SchedulingWithIndividualDeadlines { // 3 tasks, 2 processors, individual deadlines [2, 2, 3], precedence: 0→2 - SchedulingWithIndividualDeadlines::new(3, 2, vec![2, 2, 3], vec![(0, 2)]) + SchedulingWithIndividualDeadlines::new(3, 2, vec![2, 2, 3], vec![(0, 2)]).unwrap() } fn infeasible_instance() -> SchedulingWithIndividualDeadlines { // 3 tasks, 1 processor, deadlines [1, 1, 1] → only 1 slot, can't fit 3 tasks - SchedulingWithIndividualDeadlines::new(3, 1, vec![1, 1, 1], vec![]) + SchedulingWithIndividualDeadlines::new(3, 1, vec![1, 1, 1], vec![]).unwrap() } #[test] @@ -31,7 +31,7 @@ fn test_schedulingwithindividualdeadlines_to_ilp_structure() { #[test] fn test_schedulingwithindividualdeadlines_to_ilp_fixes_unused_slots() { - let problem = SchedulingWithIndividualDeadlines::new(2, 2, vec![1, 2], vec![]); + let problem = SchedulingWithIndividualDeadlines::new(2, 2, vec![1, 2], vec![]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); assert!(!reduction .target_problem() @@ -86,8 +86,9 @@ fn test_schedulingwithindividualdeadlines_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionSWIDToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance should yield infeasible ILP" ); } diff --git a/src/unit_tests/rules/sequencingtominimizetardytaskweight_ilp.rs b/src/unit_tests/rules/sequencingtominimizetardytaskweight_ilp.rs index baac86613..c90ea934a 100644 --- a/src/unit_tests/rules/sequencingtominimizetardytaskweight_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizetardytaskweight_ilp.rs @@ -134,24 +134,23 @@ fn test_tardy_ilp_signed_permutations_and_all_indicators() { (0..count).all(|job| (bits[count * count + job] == 1) == expected[job]); let value = target.evaluate(&bits).unwrap(); assert_eq!(value.is_valid(), exact); - let extracted = reduction.extract_solution(&bits); - assert_eq!(extracted.is_ok(), exact); if exact { + let extracted = reduction.extract_solution(&bits); assert_eq!(value.value, source_value.0); assert_eq!(extracted.unwrap(), schedule); } } } - assert!(reduction - .extract_solution(&vec![0; target.num_vars() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &vec![0; target.num_vars() + 1]), Ok(value) if value.is_valid()) + ); if count > 0 { - assert!(reduction - .extract_solution(&vec![0; target.num_vars()]) - .is_err()); - assert!(reduction - .extract_solution(&vec![2; target.num_vars()]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &vec![0; target.num_vars()]), Ok(value) if value.is_valid()) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &vec![2; target.num_vars()]), Ok(value) if value.is_valid()) + ); } } } @@ -170,9 +169,8 @@ fn test_tardy_ilp_complete_small_binary_target_space() { .map(|i| i64::from(mask & (1 << i) != 0)) .collect(); let value = target.evaluate(&bits).unwrap(); - let extracted = reduction.extract_solution(&bits); - assert_eq!(extracted.is_ok(), value.is_valid()); - if let Ok(schedule) = extracted { + if value.is_valid() { + let schedule = reduction.extract_solution(&bits).unwrap(); assert_eq!(source.evaluate(&schedule).unwrap().0, value.value); feasible += 1; } diff --git a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index 62b7503de..0d00777ef 100644 --- a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -104,8 +104,9 @@ fn test_cyclic_precedence_instance_is_infeasible() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); - assert!( - ILPSolver::new().solve(ilp).is_err(), + assert_eq!( + ILPSolver::new().solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible), "cyclic precedences should make the ILP infeasible" ); } diff --git a/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs b/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs index 3768789a1..920769a9b 100644 --- a/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -7,7 +7,8 @@ use crate::types::Or; #[test] fn test_sequencingtominimizeweightedtardiness_to_ilp_closed_loop() { let problem = - SequencingToMinimizeWeightedTardiness::new(vec![3, 4, 2], vec![2, 3, 1], vec![5, 8, 4], 10); + SequencingToMinimizeWeightedTardiness::new(vec![3, 4, 2], vec![2, 3, 1], vec![5, 8, 4], 10) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Use ILPSolver directly (BruteForce cannot enumerate `ILP`) @@ -21,7 +22,8 @@ fn test_sequencingtominimizeweightedtardiness_to_ilp_closed_loop() { #[test] fn test_sequencingtominimizeweightedtardiness_to_ilp_bf_vs_ilp() { let problem = - SequencingToMinimizeWeightedTardiness::new(vec![3, 4, 2], vec![2, 3, 1], vec![5, 8, 4], 10); + SequencingToMinimizeWeightedTardiness::new(vec![3, 4, 2], vec![2, 3, 1], vec![5, 8, 4], 10) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf_witness = BruteForce::new() @@ -41,10 +43,12 @@ fn test_sequencingtominimizeweightedtardiness_to_ilp_bf_vs_ilp() { fn test_sequencingtominimizeweightedtardiness_to_ilp_infeasible() { // All jobs have length 10, deadline 1, weight 1, bound 0: impossible let problem = - SequencingToMinimizeWeightedTardiness::new(vec![10, 10], vec![1, 1], vec![1, 1], 0); + SequencingToMinimizeWeightedTardiness::new(vec![10, 10], vec![1, 1], vec![1, 1], 0) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible STMWT should produce infeasible ILP" ); } @@ -57,7 +61,8 @@ fn test_sequencingtominimizeweightedtardiness_to_ilp_no_tardiness() { vec![1, 1, 1], vec![10, 10, 10], 0, - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) diff --git a/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs b/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs index 30a4bcccc..c839db5b9 100644 --- a/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs +++ b/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs @@ -49,8 +49,9 @@ fn test_sequencingwithdeadlinesandsetuptimes_to_ilp_infeasible() { let problem = SequencingWithDeadlinesAndSetUpTimes::new(vec![2, 2], vec![1, 1], vec![0, 0], vec![0]); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/sequencingwithinintervals_ilp.rs b/src/unit_tests/rules/sequencingwithinintervals_ilp.rs index 4f4a6c7e6..aaf208a33 100644 --- a/src/unit_tests/rules/sequencingwithinintervals_ilp.rs +++ b/src/unit_tests/rules/sequencingwithinintervals_ilp.rs @@ -72,8 +72,9 @@ fn test_sequencingwithinintervals_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionSWIToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance (forced overlap) should yield infeasible ILP" ); } diff --git a/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs b/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs index 2598ef274..831647c23 100644 --- a/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs +++ b/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs @@ -8,7 +8,8 @@ use crate::types::Or; #[test] fn test_sequencingwithreleasetimesanddeadlines_to_ilp_closed_loop() { let problem = - SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]); + SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_bf_vs_ilp(&problem, &reduction); @@ -17,7 +18,8 @@ fn test_sequencingwithreleasetimesanddeadlines_to_ilp_closed_loop() { #[test] fn test_sequencingwithreleasetimesanddeadlines_to_ilp_bf_vs_ilp() { let problem = - SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]); + SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf_witness = BruteForce::new() @@ -36,10 +38,12 @@ fn test_sequencingwithreleasetimesanddeadlines_to_ilp_bf_vs_ilp() { #[test] fn test_sequencingwithreleasetimesanddeadlines_to_ilp_infeasible() { // Two tasks that can't both fit: both need time 0-1, but overlap - let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 0], vec![2, 2]); + let problem = + SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 0], vec![2, 2]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible SWRTD should produce infeasible ILP" ); } @@ -48,18 +52,19 @@ fn test_sequencingwithreleasetimesanddeadlines_to_ilp_infeasible() { fn test_sequencingwithreleasetimesanddeadlines_to_ilp_rejects_empty_start_window() { // Task 0 cannot meet its deadline even when it starts immediately. Its // admissible start-time set is empty, rather than the singleton {0}. - let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![14], vec![0], vec![13]); + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![14], vec![0], vec![13]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "a task longer than its release-deadline window must make the ILP infeasible" ); } #[test] fn test_sequencingwithreleasetimesanddeadlines_to_ilp_single_task() { - let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![3], vec![1], vec![5]); + let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![3], vec![1], vec![5]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) diff --git a/src/unit_tests/rules/setsplitting_ilp.rs b/src/unit_tests/rules/setsplitting_ilp.rs index c4cb8d97c..ef7505faf 100644 --- a/src/unit_tests/rules/setsplitting_ilp.rs +++ b/src/unit_tests/rules/setsplitting_ilp.rs @@ -70,8 +70,9 @@ fn test_setsplitting_to_ilp_infeasible() { let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); - assert!( - ilp_solver.solve(ilp).is_err(), + assert_eq!( + ilp_solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible), "ILP should be infeasible for unsplittable instance" ); } diff --git a/src/unit_tests/rules/shortestcommonsupersequence_ilp.rs b/src/unit_tests/rules/shortestcommonsupersequence_ilp.rs index 80ed3daef..c186d9e39 100644 --- a/src/unit_tests/rules/shortestcommonsupersequence_ilp.rs +++ b/src/unit_tests/rules/shortestcommonsupersequence_ilp.rs @@ -7,7 +7,7 @@ use crate::traits::Problem; fn test_reduction_creates_valid_ilp() { // Alphabet {0,1}, strings [0,1] and [1,0] // max_length = 2 + 2 = 4, k = 3 (alphabet_size + 1 for padding) - let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]]); + let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]]).unwrap(); let reduction: ReductionSCSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -20,7 +20,7 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_shortestcommonsupersequence_to_ilp_closed_loop() { - let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]]); + let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]]).unwrap(); let bf_value_solution = BruteForce::new().solve(&problem).unwrap().unwrap(); let bf_value = problem.evaluate(&bf_value_solution).unwrap(); @@ -39,7 +39,7 @@ fn test_shortestcommonsupersequence_to_ilp_closed_loop() { #[test] fn test_shortestcommonsupersequence_to_ilp_bf_vs_ilp() { - let problem = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2], vec![2, 1, 0]]); + let problem = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2], vec![2, 1, 0]]).unwrap(); let bf = BruteForce::new(); let bf_witness = bf.solve(&problem).unwrap(); assert!(bf_witness.is_some()); @@ -57,7 +57,7 @@ fn test_shortestcommonsupersequence_to_ilp_bf_vs_ilp() { #[test] fn test_solution_extraction() { // Single string [0,1] - let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1]]); + let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1]]).unwrap(); let reduction: ReductionSCSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); diff --git a/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs b/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs index 8fe434aab..128074f4a 100644 --- a/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs @@ -8,13 +8,14 @@ use crate::types::Min; /// 3-vertex path: 0 -- 1 -- 2, s=0, t=2. fn simple_path_problem() -> ShortestWeightConstrainedPath { ShortestWeightConstrainedPath::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![2, 3], vec![1, 2], 0, 2, 4, // weight_bound ) + .unwrap() } #[test] @@ -37,13 +38,14 @@ fn test_reduction_creates_valid_ilp() { fn test_shortestweightconstrainedpath_to_ilp_bf_vs_ilp() { // Larger instance with multiple paths let problem = ShortestWeightConstrainedPath::new( - SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (3, 4)]).unwrap(), vec![2, 5, 3, 1, 2], // lengths vec![3, 1, 2, 4, 1], // weights 0, 4, 10, // weight_bound - ); + ) + .unwrap(); let bf = BruteForce::new(); let bf_value_solution = bf.solve(&problem).unwrap().unwrap(); @@ -61,10 +63,11 @@ fn test_shortestweightconstrainedpath_to_ilp_bf_vs_ilp() { // Both should agree on the optimal length assert_eq!(ilp_value, bf_value); } - Err(_) => { + Err(crate::solvers::ILPSolveError::Infeasible) => { // ILP found no feasible solution; brute force should agree assert_eq!(bf_value, Min(None)); } + Err(error) => panic!("ILP execution failed: {error}"), } } @@ -88,13 +91,14 @@ fn test_solution_extraction() { fn test_shortestweightconstrainedpath_to_ilp_trivial() { // s == t: trivially feasible (empty path, zero length) let problem = ShortestWeightConstrainedPath::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![2, 3], vec![1, 2], 1, 1, 4, // weight_bound - ); + ) + .unwrap(); let reduction: ReductionSWCPToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); diff --git a/src/unit_tests/rules/sparsematrixcompression_ilp.rs b/src/unit_tests/rules/sparsematrixcompression_ilp.rs index ebc2628b0..2c3f29311 100644 --- a/src/unit_tests/rules/sparsematrixcompression_ilp.rs +++ b/src/unit_tests/rules/sparsematrixcompression_ilp.rs @@ -16,7 +16,8 @@ fn test_smc_to_ilp_structure() { vec![true, false, false, false], ], 2, - ); + ) + .unwrap(); let reduction: ReductionSMCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -35,7 +36,8 @@ fn test_smc_to_ilp_closed_loop() { vec![true, false, false, false], ], 2, - ); + ) + .unwrap(); let reduction: ReductionSMCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_bf_vs_ilp(&problem, &reduction); @@ -51,7 +53,8 @@ fn test_smc_to_ilp_bf_vs_ilp() { vec![true, false, false, false], ], 2, - ); + ) + .unwrap(); let reduction: ReductionSMCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -70,7 +73,7 @@ fn test_smc_to_ilp_bf_vs_ilp() { #[test] fn test_smc_to_ilp_trivial() { // Single row, K=1 - let problem = SparseMatrixCompression::new(vec![vec![true, false]], 1); + let problem = SparseMatrixCompression::new(vec![vec![true, false]], 1).unwrap(); let reduction: ReductionSMCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); diff --git a/src/unit_tests/rules/spinglass_maxcut.rs b/src/unit_tests/rules/spinglass_maxcut.rs index 75faf6fd8..ce0d4d0c5 100644 --- a/src/unit_tests/rules/spinglass_maxcut.rs +++ b/src/unit_tests/rules/spinglass_maxcut.rs @@ -1,7 +1,7 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; -include!("../jl_helpers.rs"); #[test] fn test_spinglass_to_maxcut_closed_loop() { @@ -58,7 +58,11 @@ fn test_solution_extraction_with_ancilla() { #[test] fn test_weighted_maxcut() { - let mc = MaxCut::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![10, 20]); + let mc = MaxCut::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![10, 20], + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&mc).expect("reduction should succeed"); let sg = reduction.target_problem(); @@ -71,7 +75,7 @@ fn test_weighted_maxcut() { #[test] fn test_reduction_structure() { // Test MaxCut to SpinGlass structure - let mc = MaxCut::<_, i64>::unweighted(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let mc = MaxCut::<_, i64>::unweighted(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let reduction = ReduceTo::>::reduce_to(&mc).expect("reduction should succeed"); let sg = reduction.target_problem(); @@ -134,7 +138,7 @@ fn test_jl_parity_maxcut_to_spinglass() { let weighted_edges = jl_parse_weighted_edges(inst); let edges: Vec<(usize, usize)> = weighted_edges.iter().map(|&(u, v, _)| (u, v)).collect(); let weights: Vec = weighted_edges.into_iter().map(|(_, _, w)| w).collect(); - let source = MaxCut::new(SimpleGraph::new(nv, edges), weights); + let source = MaxCut::new(SimpleGraph::new(nv, edges).unwrap(), weights).unwrap(); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let solver = BruteForce::new(); @@ -166,9 +170,10 @@ fn test_jl_parity_rule_maxcut_to_spinglass() { let edges: Vec<(usize, usize)> = weighted_edges.iter().map(|&(u, v, _)| (u, v)).collect(); let weights: Vec = weighted_edges.into_iter().map(|(_, _, w)| w).collect(); let source = MaxCut::new( - SimpleGraph::new(inst["num_vertices"].as_u64().unwrap() as usize, edges), + SimpleGraph::new(inst["num_vertices"].as_u64().unwrap() as usize, edges).unwrap(), weights, - ); + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let solver = BruteForce::new(); diff --git a/src/unit_tests/rules/spinglass_qubo.rs b/src/unit_tests/rules/spinglass_qubo.rs index 7894a553b..4d1443b1d 100644 --- a/src/unit_tests/rules/spinglass_qubo.rs +++ b/src/unit_tests/rules/spinglass_qubo.rs @@ -1,8 +1,9 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; -include!("../jl_helpers.rs"); +use crate::traits::Problem; #[test] fn test_spinglass_to_qubo_closed_loop() { @@ -68,7 +69,7 @@ fn test_reduction_structure() { let reduction2 = ReduceTo::>::reduce_to(&sg2).expect("reduction should succeed"); let qubo2 = reduction2.target_problem(); - assert_eq!(qubo2.num_variables(), 3); + assert_eq!(qubo2.num_variables().unwrap(), 3); } #[test] @@ -207,3 +208,30 @@ fn test_jl_parity_rule_qubo_to_spinglass() { assert_eq!(best_source, jl_parse_bool_configs_set(&case["best_source"])); } } + +#[test] +fn test_qubo_to_spinglass_preserves_small_nonzero_coefficients() { + // Exact powers of two distinguish algebraic coefficient preservation from + // backend tolerances. The two scales expose both former pruning branches: + // q < 1e-10, and q > 1e-10 but q/4 < 1e-10. + for magnitude in [2.0_f64.powi(-40), 2.0_f64.powi(-32)] { + for sign in [-1.0, 1.0] { + let q = sign * magnitude; + let source = QUBO::::from_matrix(vec![vec![q, q], vec![0.0, 0.0]]).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = reduction.target_problem(); + assert_eq!(target.fields(), &[3.0 * q / 4.0, q / 4.0]); + assert_eq!(target.interactions(), vec![((0, 1), q / 4.0)]); + let offset = 3.0 * q / 4.0; + for left in [-1, 1] { + for right in [-1, 1] { + let spins = vec![left, right]; + let bits = reduction.extract_solution(&spins).unwrap(); + let source_value = source.evaluate(&bits).unwrap().0.unwrap(); + let target_value = target.evaluate(&spins).unwrap().0.unwrap(); + assert_eq!(source_value, target_value + offset); + } + } + } + } +} diff --git a/src/unit_tests/rules/steinertree_ilp.rs b/src/unit_tests/rules/steinertree_ilp.rs index 6b98b506c..a4b3c5cd4 100644 --- a/src/unit_tests/rules/steinertree_ilp.rs +++ b/src/unit_tests/rules/steinertree_ilp.rs @@ -10,6 +10,7 @@ fn lift(source: &SteinerTree, chosen: &[bool]) -> Vec { let root = source.terminals()[0]; let edges = source.graph().edges(); let mut witness = vec![0; tree_ilp_sizes(n, m, source.terminals().len()).unwrap().0]; + witness[m + root] = 1; let mut adj = vec![vec![]; n]; for (e, &(u, v)) in edges.iter().enumerate() { if chosen[e] { @@ -65,8 +66,11 @@ fn test_steinertree_to_ilp_closed_loop() { (4, vec![(0, 1), (2, 3)], vec![1, -10], vec![0, 1], 1), (3, vec![(0, 1), (1, 2)], vec![1, -5], vec![0, 1], -4), (3, vec![(0, 1), (1, 2)], vec![2, 3], vec![2, 0], 5), + (1, vec![], vec![], vec![0], 0), + (2, vec![(0, 1)], vec![5], vec![0], 0), + (2, vec![(0, 1)], vec![-5], vec![0], -5), ] { - let source = SteinerTree::new(SimpleGraph::new(n, edges), weights, terminals); + let source = SteinerTree::new(SimpleGraph::new(n, edges).unwrap(), weights, terminals); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let witness = ILPSolver::new().solve(reduction.target_problem()).unwrap(); let decoded = reduction.extract_solution(&witness).unwrap(); @@ -77,7 +81,11 @@ fn test_steinertree_to_ilp_closed_loop() { ); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } - let source = SteinerTree::new(SimpleGraph::new(3, vec![(0, 1)]), vec![-1], vec![0, 2]); + let source = SteinerTree::new( + SimpleGraph::new(3, vec![(0, 1)]).unwrap(), + vec![-1], + vec![0, 2], + ); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); assert!(matches!( ILPSolver::new().solve(reduction.target_problem()), @@ -88,7 +96,7 @@ fn test_steinertree_to_ilp_closed_loop() { #[test] fn test_steiner_all_source_trees_lift_and_preserve_objective() { let source = SteinerTree::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), vec![-2, 0, 3, -1, 2, 0], vec![2, 0], ); @@ -110,7 +118,11 @@ fn test_steiner_all_source_trees_lift_and_preserve_objective() { #[test] fn test_steiner_every_small_raw_target_and_malformed_witness() { - let source = SteinerTree::new(SimpleGraph::new(2, vec![(0, 1)]), vec![-3], vec![1, 0]); + let source = SteinerTree::new( + SimpleGraph::new(2, vec![(0, 1)]).unwrap(), + vec![-3], + vec![1, 0], + ); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); let mut feasible_count = 0; @@ -121,7 +133,9 @@ fn test_steiner_every_small_raw_target_and_malformed_witness() { let decoded = reduction.extract_solution(&witness).unwrap(); assert_eq!(source.evaluate(&decoded).unwrap(), Min(Some(-3))); } else { - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &witness), Ok(value) if value.is_valid()) + ); } } for bad in [ @@ -129,7 +143,9 @@ fn test_steiner_every_small_raw_target_and_malformed_witness() { vec![1; target.num_vars() + 1], vec![2; target.num_vars()], ] { - assert!(reduction.extract_solution(&bad).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &bad), Ok(value) if value.is_valid()) + ); } assert_eq!(feasible_count, 1); } @@ -151,3 +167,21 @@ fn test_steiner_count_boundaries() { )); } } + +#[test] +fn test_single_terminal_tree_lifts_include_empty_tree() { + let source = SteinerTree::new( + SimpleGraph::new(2, vec![(0, 1)]).unwrap(), + vec![-5], + vec![1], + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + for selected in [vec![false], vec![true]] { + let witness = lift(&source, &selected); + assert_eq!( + reduction.target_problem().evaluate(&witness).unwrap().value, + source.evaluate(&selected).unwrap().0 + ); + assert_eq!(reduction.extract_solution(&witness).unwrap(), selected); + } +} diff --git a/src/unit_tests/rules/steinertreeingraphs_ilp.rs b/src/unit_tests/rules/steinertreeingraphs_ilp.rs deleted file mode 100644 index d09885b41..000000000 --- a/src/unit_tests/rules/steinertreeingraphs_ilp.rs +++ /dev/null @@ -1,30 +0,0 @@ -use super::*; -use crate::models::algebraic::ILP; -use crate::rules::test_helpers::assert_bf_vs_ilp; -use crate::rules::ReduceTo; -use crate::topology::SimpleGraph; - -#[test] -fn test_steinertreeingraphs_to_ilp_closed_loop() { - // Path graph: 0 - 1 - 2, terminals {0, 2}, weights [1, 1] - // Optimal Steiner tree: use both edges (cost 2) - // ILP variables: 2 + 2*2*1 = 6 binary = 64 configs - let source = SteinerTreeInGraphs::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), - vec![0, 2], - vec![1, 1], - ); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert_bf_vs_ilp(&source, &reduction); -} - -#[test] -fn test_steinertreeingraphs_to_ilp_bf_vs_ilp() { - let source = SteinerTreeInGraphs::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), - vec![0, 2], - vec![1, 1], - ); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); -} diff --git a/src/unit_tests/rules/stringtostringcorrection_ilp.rs b/src/unit_tests/rules/stringtostringcorrection_ilp.rs index 83354d82f..9a4b5a585 100644 --- a/src/unit_tests/rules/stringtostringcorrection_ilp.rs +++ b/src/unit_tests/rules/stringtostringcorrection_ilp.rs @@ -7,7 +7,7 @@ use crate::types::Or; #[test] fn test_reduction_creates_valid_ilp() { // source = [0,1], target = [1], bound = 1 (delete position 0) - let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1], 1); + let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1], 1).unwrap(); let reduction: ReductionSTSCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -20,7 +20,7 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_stringtostringcorrection_to_ilp_bf_vs_ilp() { // source=[0,1], target=[1], bound=1 (delete position 0) - let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1], 1); + let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1], 1).unwrap(); let bf = BruteForce::new(); let bf_witness = bf.solve(&problem).unwrap(); @@ -39,7 +39,7 @@ fn test_stringtostringcorrection_to_ilp_bf_vs_ilp() { #[test] fn test_solution_extraction_delete() { // source=[0,1], target=[1], bound=1 => delete at position 0 - let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1], 1); + let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1], 1).unwrap(); let reduction: ReductionSTSCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); @@ -55,7 +55,7 @@ fn test_solution_extraction_delete() { fn test_stringtostringcorrection_to_ilp_infeasible() { // source=[0], target=[0,1]: m > n, so model rejects before any search // The ILP is trivially infeasible (0 vars, unsatisfiable constraint) - let problem = StringToStringCorrection::new(2, vec![0], vec![0, 1], 1); + let problem = StringToStringCorrection::new(2, vec![0], vec![0, 1], 1).unwrap(); // Verify the source problem is actually infeasible let bf = BruteForce::new(); @@ -65,8 +65,9 @@ fn test_stringtostringcorrection_to_ilp_infeasible() { let reduction: ReductionSTSCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); - assert!( - ilp_solver.solve(reduction.target_problem()).is_err(), + assert_eq!( + ilp_solver.solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "reduced ILP should also be infeasible" ); } @@ -74,7 +75,7 @@ fn test_stringtostringcorrection_to_ilp_infeasible() { #[test] fn test_stringtostringcorrection_to_ilp_swap() { // source=[1,0], target=[0,1], bound=1 => swap at position 0 - let problem = StringToStringCorrection::new(2, vec![1, 0], vec![0, 1], 1); + let problem = StringToStringCorrection::new(2, vec![1, 0], vec![0, 1], 1).unwrap(); let bf = BruteForce::new(); let bf_witness = bf.solve(&problem).unwrap(); diff --git a/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs b/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs index 0fd893b32..53c0416b3 100644 --- a/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs @@ -9,7 +9,7 @@ use crate::traits::Problem; fn small_instance() -> StrongConnectivityAugmentation { // Path 0->1->2, candidates: (2,0,1),(1,0,2), bound=2 StrongConnectivityAugmentation::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![(2, 0, 1), (1, 0, 2)], 2, ) @@ -53,7 +53,8 @@ fn test_extract_solution() { #[test] fn test_trivial_single_vertex() { - let source = StrongConnectivityAugmentation::new(DirectedGraph::new(1, vec![]), vec![], 0); + let source = + StrongConnectivityAugmentation::new(DirectedGraph::new(1, vec![]).unwrap(), vec![], 0); let reduction: ReductionSCAToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -65,8 +66,11 @@ fn test_trivial_single_vertex() { #[test] fn test_single_vertex_candidate_selection_must_still_respect_budget() { - let source = - StrongConnectivityAugmentation::new(DirectedGraph::new(1, vec![]), vec![(0, 0, 1)], 0); + let source = StrongConnectivityAugmentation::new( + DirectedGraph::new(1, vec![]).unwrap(), + vec![(0, 0, 1)], + 0, + ); let reduction: ReductionSCAToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -87,7 +91,7 @@ fn test_single_vertex_candidate_selection_must_still_respect_budget() { fn test_infeasible_budget() { // 3 vertices 0->1->2, only candidate is (2,0,10), budget=5 let source = StrongConnectivityAugmentation::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![(2, 0, 10)], 5, ); @@ -95,7 +99,10 @@ fn test_infeasible_budget() { ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); - assert!(solver.solve(ilp).is_err()); + assert_eq!( + solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/subgraphisomorphism_ilp.rs b/src/unit_tests/rules/subgraphisomorphism_ilp.rs index cd7fe8f07..10aa07cab 100644 --- a/src/unit_tests/rules/subgraphisomorphism_ilp.rs +++ b/src/unit_tests/rules/subgraphisomorphism_ilp.rs @@ -7,8 +7,8 @@ use crate::types::Or; #[test] fn test_reduction_creates_valid_ilp() { // Host: K4, Pattern: K3 - let host = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); - let pattern = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); + let host = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); + let pattern = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); let reduction: ReductionSubIsoToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -21,8 +21,8 @@ fn test_reduction_creates_valid_ilp() { #[test] fn test_subgraphisomorphism_to_ilp_closed_loop() { // Host: K4, Pattern: K3 (always embeddable) - let host = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); - let pattern = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); + let host = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); + let pattern = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); // BruteForce on source to confirm feasibility @@ -51,8 +51,8 @@ fn test_subgraphisomorphism_to_ilp_closed_loop() { #[test] fn test_subgraphisomorphism_to_ilp_path_in_cycle() { // Host: C4, Pattern: P3 (path on 3 vertices) - let host = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]); - let pattern = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let host = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(); + let pattern = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); // BruteForce on source @@ -77,20 +77,24 @@ fn test_subgraphisomorphism_to_ilp_path_in_cycle() { #[test] fn test_subgraphisomorphism_to_ilp_infeasible() { // Host: path 0-1-2, Pattern: triangle K3 (not embeddable) - let host = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let pattern = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); + let host = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let pattern = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); let reduction: ReductionSubIsoToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(reduction.target_problem()); - assert!(result.is_err(), "K3 in path should be infeasible"); + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), + "K3 in path should be infeasible" + ); } #[test] fn test_solution_extraction() { - let host = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); - let pattern = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); + let host = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); + let pattern = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); let reduction: ReductionSubIsoToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -104,8 +108,8 @@ fn test_solution_extraction() { #[test] fn test_subgraphisomorphism_to_ilp_bf_vs_ilp() { - let host = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]); - let pattern = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let host = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(); + let pattern = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let problem = SubgraphIsomorphism::new(host, pattern); let reduction: ReductionSubIsoToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); diff --git a/src/unit_tests/rules/subsetsum_closestvectorproblem.rs b/src/unit_tests/rules/subsetsum_closestvectorproblem.rs index f6f4b2f9a..c865f02e8 100644 --- a/src/unit_tests/rules/subsetsum_closestvectorproblem.rs +++ b/src/unit_tests/rules/subsetsum_closestvectorproblem.rs @@ -4,7 +4,7 @@ use crate::traits::Problem; #[test] fn test_subsetsum_to_closestvectorproblem_closed_loop() { - let source = SubsetSum::new(vec![3u32, 7, 1, 8], 11u32); + let source = SubsetSum::new(vec![3u32, 7, 1, 8], 11u32).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target_solution = crate::solvers::customized::closest_vector_problem::solve(reduction.target_problem()) @@ -18,18 +18,19 @@ fn test_subsetsum_to_closestvectorproblem_closed_loop() { .evaluate(&target_solution) .unwrap() .0, - Some(2.0) + Some(BigRational::from_integer(4.into())) ); } #[test] fn test_subsetsum_to_closestvectorproblem_structure() { - let source = SubsetSum::new(vec![3u32, 7, 1, 8], 11u32); + let source = SubsetSum::new(vec![3u32, 7, 1, 8], 11u32).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); - let expected: serde_json::Value = serde_json::json!({"basis": [[1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1], [0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1], [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1], [0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 1, -2, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -2, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -2]], "target": [0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1]}); - assert_eq!(serde_json::to_value(target).unwrap(), expected); + assert_eq!(target.num_basis_vectors(), 7); + assert_eq!(target.ambient_dimension(), 12); + assert_eq!(&target.target()[..8], &[0, 0, 0, 0, 1, 1, 1, 1]); assert_eq!( ClosestVectorProblem::::variant(), vec![("target", "i64")] @@ -38,12 +39,15 @@ fn test_subsetsum_to_closestvectorproblem_structure() { #[test] fn test_subsetsum_to_closestvectorproblem_binary_minimizers() { - let source = SubsetSum::new(vec![3u32, 7, 1, 8], 11u32); + let source = SubsetSum::new(vec![3u32, 7, 1, 8], 11u32).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); for solution in [vec![1, 0, 0, 1, 0, 0, 0], vec![1, 1, 1, 0, 1, 1, 1]] { - assert_eq!(target.evaluate(&solution).unwrap().0, Some(2.0)); + assert_eq!( + target.evaluate(&solution).unwrap().0, + Some(BigRational::from_integer(4.into())) + ); assert!( source .evaluate(&reduction.extract_solution(&solution).unwrap()) @@ -55,7 +59,7 @@ fn test_subsetsum_to_closestvectorproblem_binary_minimizers() { #[test] fn test_subsetsum_to_closestvectorproblem_unsatisfiable_instance() { - let source = SubsetSum::new(vec![2u32, 4, 6], 5u32); + let source = SubsetSum::new(vec![2u32, 4, 6], 5u32).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let solution = crate::solvers::customized::closest_vector_problem::solve(reduction.target_problem()) @@ -66,21 +70,21 @@ fn test_subsetsum_to_closestvectorproblem_unsatisfiable_instance() { .evaluate(&solution) .unwrap() .unwrap() - > (source.num_elements() as f64).sqrt() + > BigRational::from_integer(source.num_elements().into()) ); } #[test] -fn test_subsetsum_to_closestvectorproblem_large_integers_and_unit_pivots() { +fn test_subsetsum_to_closestvectorproblem_binary_carries_preserve_large_inputs() { use num_bigint::BigUint; let size = BigUint::from(1u32) << 70usize; - let source = SubsetSum::new(vec![size.clone()], size); + let source = SubsetSum::new(vec![size.clone()], size).unwrap(); let result = ReduceTo::>::reduce_to(&source).unwrap(); let mut witness = vec![0; result.target_problem().num_basis_vectors()]; witness[0] = 1; assert_eq!( result.target_problem().evaluate(&witness).unwrap(), - Min(Some(1.0)) + Min(Some(BigRational::from_integer(1.into()))) ); assert_eq!(result.extract_solution(&witness).unwrap(), vec![true]); assert!(result @@ -90,7 +94,7 @@ fn test_subsetsum_to_closestvectorproblem_large_integers_and_unit_pivots() { .flatten() .all(|&x| (-2..=1).contains(&x))); - let source = SubsetSum::new(vec![1u32; 40], 20u32); + let source = SubsetSum::new(vec![1u32; 40], 20u32).unwrap(); let result = ReduceTo::>::reduce_to(&source).unwrap(); let mut witness = vec![0; result.target_problem().num_basis_vectors()]; witness[..20].fill(1); @@ -113,7 +117,7 @@ fn test_subsetsum_to_closestvectorproblem_all_small_coefficients() { (vec![3, 7, 1], 11), (vec![2, 4], 5), ] { - let source = SubsetSum::new(sizes, target_sum); + let source = SubsetSum::new(sizes, target_sum).unwrap(); let result = ReduceTo::>::reduce_to(&source).unwrap(); let target = result.target_problem(); assert!(std::ptr::eq( @@ -132,18 +136,18 @@ fn test_subsetsum_to_closestvectorproblem_all_small_coefficients() { }) .collect(); let value = target.evaluate(&config).unwrap(); - let certificate = value == Min(Some(result.target_distance)); + let certificate = value + == Min(Some(BigRational::from_integer( + source.num_elements().into(), + ))); assert_eq!( crate::rules::AggregateReductionResult::extract_value(&result, value), Or(certificate) ); - match result.extract_solution(&config) { - Ok(x) => { - assert!(certificate); - assert!(source.evaluate(&x).unwrap().0); - accepted = true; - } - Err(_) => assert!(!certificate), + if certificate { + let x = result.extract_solution(&config).unwrap(); + assert!(source.evaluate(&x).unwrap().0); + accepted = true; } } assert_eq!( @@ -153,7 +157,9 @@ fn test_subsetsum_to_closestvectorproblem_all_small_coefficients() { .unwrap() .is_some() ); - assert!(result.extract_solution(&vec![0; dimensions + 1]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&result), &vec![0; dimensions + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&result, value.clone()); value.is_valid() }) + ); assert_eq!( crate::rules::AggregateReductionResult::extract_value(&result, Min(None)), Or(false) diff --git a/src/unit_tests/rules/subsetsum_integerexpressionmembership.rs b/src/unit_tests/rules/subsetsum_integerexpressionmembership.rs index 9caba9ab6..02a7b6d70 100644 --- a/src/unit_tests/rules/subsetsum_integerexpressionmembership.rs +++ b/src/unit_tests/rules/subsetsum_integerexpressionmembership.rs @@ -10,7 +10,7 @@ use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_example_source() -> SubsetSum { - SubsetSum::new(vec![1u32, 5, 6, 8], 11u32) + SubsetSum::new(vec![1u32, 5, 6, 8], 11u32).unwrap() } fn issue_example_source_config() -> Vec { @@ -52,17 +52,15 @@ fn test_subsetsum_to_integerexpressionmembership_extract_solution_matches_choice .unwrap(), issue_example_source_config() ); - assert_eq!( - reduction - .extract_solution(&vec![true, false, false, true]) - .unwrap(), - vec![true, false, false, true] + // Selecting 1 and 8 does not reach the source target 11. + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true, false, false, true]), Ok(value) if { value.is_valid() }) ); } #[test] fn test_subsetsum_to_integerexpressionmembership_unsatisfiable_instance_stays_unsatisfiable() { - let source = SubsetSum::new(vec![2u32, 4, 6], 5u32); + let source = SubsetSum::new(vec![2u32, 4, 6], 5u32).unwrap(); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); diff --git a/src/unit_tests/rules/subsetsum_integerknapsack.rs b/src/unit_tests/rules/subsetsum_integerknapsack.rs index 15a2978f8..574cd2bd6 100644 --- a/src/unit_tests/rules/subsetsum_integerknapsack.rs +++ b/src/unit_tests/rules/subsetsum_integerknapsack.rs @@ -36,7 +36,7 @@ fn subset_sum_embedding(source: &SubsetSum) -> IntegerKnapsack { #[test] fn test_subsetsum_to_integerknapsack_forward_example() { - let source = SubsetSum::new(vec![3u32, 7, 1, 8, 5], 16u32); + let source = SubsetSum::new(vec![3u32, 7, 1, 8, 5], 16u32).unwrap(); let target = subset_sum_embedding(&source); let source_witness = vec![true, false, false, true, true]; @@ -47,7 +47,7 @@ fn test_subsetsum_to_integerknapsack_forward_example() { #[test] fn test_subsetsum_to_integerknapsack_counterexample_demonstrates_gap() { - let source = SubsetSum::new(vec![3u32], 6u32); + let source = SubsetSum::new(vec![3u32], 6u32).unwrap(); let target = subset_sum_embedding(&source); let solver = BruteForce::new(); diff --git a/src/unit_tests/rules/subsetsum_partition.rs b/src/unit_tests/rules/subsetsum_partition.rs index 663dbd54e..fa363e0df 100644 --- a/src/unit_tests/rules/subsetsum_partition.rs +++ b/src/unit_tests/rules/subsetsum_partition.rs @@ -10,7 +10,7 @@ use crate::traits::Problem; #[test] fn test_subsetsum_to_partition_closed_loop() { - let source = SubsetSum::new(vec![1u32, 5, 6, 8], 11u32); + let source = SubsetSum::new(vec![1u32, 5, 6, 8], 11u32).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); @@ -26,7 +26,7 @@ fn test_subsetsum_to_partition_closed_loop() { #[test] fn test_subsetsum_to_partition_sigma_greater_than_two_t_extraction() { - let source = SubsetSum::new(vec![10u32, 20, 30], 10u32); + let source = SubsetSum::new(vec![10u32, 20, 30], 10u32).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); assert_eq!(reduction.target_problem().sizes(), &[10, 20, 30, 40]); @@ -46,7 +46,7 @@ fn test_subsetsum_to_partition_sigma_greater_than_two_t_extraction() { #[test] fn test_subsetsum_to_partition_sigma_equals_two_t_extraction() { - let source = SubsetSum::new(vec![3u32, 5, 2, 6], 8u32); + let source = SubsetSum::new(vec![3u32, 5, 2, 6], 8u32).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); assert_eq!(reduction.target_problem().sizes(), &[3, 5, 2, 6]); @@ -60,7 +60,7 @@ fn test_subsetsum_to_partition_sigma_equals_two_t_extraction() { #[test] fn test_subsetsum_to_partition_unsatisfiable_instance_stays_unsatisfiable() { - let source = SubsetSum::new(vec![3u32, 7, 11], 5u32); + let source = SubsetSum::new(vec![3u32, 7, 11], 5u32).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/unit_tests/rules/sumofsquarespartition_ilp.rs b/src/unit_tests/rules/sumofsquarespartition_ilp.rs index 5827569bb..862423737 100644 --- a/src/unit_tests/rules/sumofsquarespartition_ilp.rs +++ b/src/unit_tests/rules/sumofsquarespartition_ilp.rs @@ -58,12 +58,19 @@ fn test_solution_extraction() { // element 0→g0, element 1→g1, element 2→g1, element 3→g0 // x_{0,0}=1,x_{0,1}=0, x_{1,0}=0,x_{1,1}=1, x_{2,0}=0,x_{2,1}=1, x_{3,0}=1,x_{3,1}=0 - // Set x vars, leave z vars as 0 for extraction test + // Set assignment variables and their within-group products. let mut ilp_solution = vec![0_i64; 4 * 2 + 4 * 4 * 2]; ilp_solution[0] = 1; // x_{0,0} ilp_solution[3] = 1; // x_{1,1} ilp_solution[5] = 1; // x_{2,1} ilp_solution[6] = 1; // x_{3,0} + for (group, members) in [(0, [0, 3]), (1, [1, 2])] { + for i in members { + for j in members { + ilp_solution[8 + (i * 4 + j) * 2 + group] = 1; + } + } + } let extracted = reduction.extract_solution(&ilp_solution).unwrap(); assert_eq!(extracted, vec![0, 1, 1, 0]); } diff --git a/src/unit_tests/rules/threedimensionalmatching_ilp.rs b/src/unit_tests/rules/threedimensionalmatching_ilp.rs index aaec26d1e..811d87452 100644 --- a/src/unit_tests/rules/threedimensionalmatching_ilp.rs +++ b/src/unit_tests/rules/threedimensionalmatching_ilp.rs @@ -3,7 +3,7 @@ use crate::models::algebraic::{Comparison, ObjectiveSense, ILP}; use crate::models::misc::{ResourceConstrainedScheduling, ThreePartition}; use crate::models::set::ThreeDimensionalMatching; use crate::rules::{ReduceTo, ReductionGraph, ReductionResult}; -use crate::solvers::{BruteForce, ILPSolveError, ILPSolver}; +use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -12,10 +12,11 @@ fn canonical_problem() -> ThreeDimensionalMatching { 3, vec![(0, 1, 2), (1, 0, 1), (2, 2, 0), (0, 0, 0), (1, 2, 2)], ) + .unwrap() } fn singleton_problem() -> ThreeDimensionalMatching { - ThreeDimensionalMatching::new(1, vec![(0, 0, 0)]) + ThreeDimensionalMatching::new(1, vec![(0, 0, 0)]).unwrap() } fn constraint_signature(constraint: &(Comparison, i64, Vec<(usize, i64)>)) -> String { @@ -104,7 +105,7 @@ fn test_threedimensionalmatching_to_ilp_closed_loop() { #[test] fn test_threedimensionalmatching_to_ilp_infeasible_instance() { - let problem = ThreeDimensionalMatching::new(2, vec![(0, 0, 0), (0, 1, 1)]); + let problem = ThreeDimensionalMatching::new(2, vec![(0, 0, 0), (0, 1, 1)]).unwrap(); let reduction: ReductionThreeDimensionalMatchingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -112,8 +113,9 @@ fn test_threedimensionalmatching_to_ilp_infeasible_instance() { BruteForce::new().solve(&problem).unwrap().is_none(), "source instance should be infeasible" ); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "reduced ILP should be infeasible" ); } @@ -138,11 +140,6 @@ fn test_threedimensionalmatching_to_ilp_direct_path_beats_indirect_chain() { let direct_source = direct.extract_solution(&direct_solution).unwrap(); assert_eq!(problem.evaluate(&direct_source).unwrap(), Or(true)); - let indirect_solution = solver.solve(indirect.target_problem()); - assert!( - matches!(indirect_solution, Err(ILPSolveError::Extraction(_))), - "the numerically unstable indirect ILP should be rejected: {indirect_solution:?}" - ); assert!(direct.target_problem().num_vars() < indirect.target_problem().num_vars()); assert!( direct.target_problem().constraints().len() < indirect.target_problem().constraints().len() diff --git a/src/unit_tests/rules/threedimensionalmatching_minimumweightdecoding.rs b/src/unit_tests/rules/threedimensionalmatching_minimumweightdecoding.rs index 9b3edb61b..b30503009 100644 --- a/src/unit_tests/rules/threedimensionalmatching_minimumweightdecoding.rs +++ b/src/unit_tests/rules/threedimensionalmatching_minimumweightdecoding.rs @@ -13,7 +13,7 @@ fn reduce_tdm( ThreeDimensionalMatching, ReductionThreeDimensionalMatchingToMinimumWeightDecoding, ) { - let source = ThreeDimensionalMatching::new(universe_size, triples); + let source = ThreeDimensionalMatching::new(universe_size, triples).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); (source, reduction) @@ -175,7 +175,7 @@ fn test_threedimensionalmatching_to_minimumweightdecoding_solution_extraction_id ); } - assert!(reduction - .extract_solution(&vec![false, true, false]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false, true, false]), Ok(value) if { value.is_valid() }) + ); } diff --git a/src/unit_tests/rules/threedimensionalmatching_threepartition.rs b/src/unit_tests/rules/threedimensionalmatching_threepartition.rs index f9cb2913b..b470025af 100644 --- a/src/unit_tests/rules/threedimensionalmatching_threepartition.rs +++ b/src/unit_tests/rules/threedimensionalmatching_threepartition.rs @@ -12,7 +12,7 @@ fn reduce( ThreeDimensionalMatching, ReductionThreeDimensionalMatchingToThreePartition, ) { - let source = ThreeDimensionalMatching::new(universe_size, triples.to_vec()); + let source = ThreeDimensionalMatching::new(universe_size, triples.to_vec()).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); (source, reduction) @@ -123,3 +123,68 @@ fn test_threedimensionalmatching_to_threepartition_uncovered_coordinate_maps_to_ "target instance should be infeasible" ); } + +#[test] +fn test_threedimensionalmatching_to_threepartition_extracts_noncanonical_partition() { + let (source, reduction) = reduce(1, &[(0, 0, 0)]); + // A mathematically valid witness found independently by HiGHS. Both regular + // triples initially contain UPrime elements; filler triples mix pair IDs. + // Keep the witness fixed so this regression does not depend on the backend. + let witness = vec![ + 4, 0, 0, 4, 2, 1, 3, 3, 6, 0, 6, 4, 5, 5, 2, 1, 3, 1, 6, 2, 5, + ]; + assert!(reduction.target_problem().evaluate(&witness).unwrap().0); + let extracted = reduction.extract_solution(&witness).unwrap(); + assert_eq!(extracted, vec![true]); + assert!(source.evaluate(&extracted).unwrap().0); + // Group labels have no mathematical significance. + let relabeled = witness.iter().map(|group| 6 - group).collect(); + assert_eq!(reduction.extract_solution(&relabeled).unwrap(), extracted); +} + +#[test] +fn test_threedimensionalmatching_to_threepartition_equal_size_permutations() { + let (source, reduction) = reduce(2, &[(0, 0, 0), (0, 1, 1), (1, 0, 0), (1, 1, 1)]); + let target = reduction.target_problem(); + for matching in [[1, 0, 0, 1], [0, 1, 1, 0]] { + let mut witness = reduction.build_target_witness(&matching); + let mut exchanges = 0; + // Cumulative equal-size exchanges preserve a valid partition while + // exercising regular-item identities, mixed fillers, and dummy groups. + for left in 0..target.num_elements() { + for right in left + 1..target.num_elements() { + if target.sizes()[left] == target.sizes()[right] && witness[left] != witness[right] + { + witness.swap(left, right); + assert!(target.evaluate(&witness).unwrap().0); + let extracted = reduction.extract_solution(&witness).unwrap(); + assert!(source.evaluate(&extracted).unwrap().0); + exchanges += 1; + } + } + } + assert!(exchanges > 0); + } +} + +#[test] +fn test_threedimensionalmatching_to_threepartition_rejects_invalid_partitions() { + let (_, reduction) = reduce(1, &[(0, 0, 0)]); + let valid = reduction.build_target_witness(&[1]); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); + let mut invalid = valid.clone(); + invalid[0] = reduction.target_problem().num_groups(); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &invalid), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; valid.len()]), Ok(value) if { value.is_valid() }) + ); + let mut wrong_sum = valid; + wrong_sum.swap(0, 2); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &wrong_sum), Ok(value) if { value.is_valid() }) + ); +} diff --git a/src/unit_tests/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs b/src/unit_tests/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs index c7b85f5c7..015232d58 100644 --- a/src/unit_tests/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs +++ b/src/unit_tests/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs @@ -2,7 +2,6 @@ use super::*; use crate::models::misc::{SequencingWithReleaseTimesAndDeadlines, ThreePartition}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; fn reduce(sizes: Vec, bound: i64) -> (ThreePartition, ReductionThreePartitionToSRTD) { @@ -91,6 +90,6 @@ fn test_threepartition_to_sequencingwithreleasetimesanddeadlines_dims() { let target = reduction.target_problem(); // 7 tasks -> Lehmer dims [7,6,5,4,3,2,1] - let dims = target.dimensions(); + let dims = crate::solvers::cartesian_dimensions(target).unwrap(); assert_eq!(dims, vec![7, 6, 5, 4, 3, 2, 1]); } diff --git a/src/unit_tests/rules/timetabledesign_ilp.rs b/src/unit_tests/rules/timetabledesign_ilp.rs index 14fd854ac..1e027b207 100644 --- a/src/unit_tests/rules/timetabledesign_ilp.rs +++ b/src/unit_tests/rules/timetabledesign_ilp.rs @@ -15,7 +15,8 @@ fn test_timetabledesign_to_ilp_closed_loop() { vec![vec![true, true], vec![true, true]], vec![vec![true, true], vec![true, true]], vec![vec![1, 0], vec![0, 1]], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_bf_vs_ilp(&problem, &reduction); @@ -30,7 +31,8 @@ fn test_timetabledesign_to_ilp_bf_vs_ilp() { vec![vec![true, true], vec![true, true]], vec![vec![true, true], vec![true, true]], vec![vec![1, 0], vec![0, 1]], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf_witness = BruteForce::new() @@ -49,10 +51,12 @@ fn test_timetabledesign_to_ilp_bf_vs_ilp() { #[test] fn test_timetabledesign_to_ilp_infeasible() { // Craftsman 0 available only in period 0, but needs 2 periods of work with task 0 - let problem = TimetableDesign::new(1, 1, 1, vec![vec![true]], vec![vec![true]], vec![vec![2]]); + let problem = + TimetableDesign::new(1, 1, 1, vec![vec![true]], vec![vec![true]], vec![vec![2]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible TD should produce infeasible ILP" ); } @@ -66,7 +70,8 @@ fn test_timetabledesign_to_ilp_identity_extraction() { vec![vec![true, true], vec![true, true]], vec![vec![true, true], vec![true, true]], vec![vec![1, 0], vec![0, 1]], - ); + ) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() diff --git a/src/unit_tests/rules/traits.rs b/src/unit_tests/rules/traits.rs index bfc64c583..587a98cbe 100644 --- a/src/unit_tests/rules/traits.rs +++ b/src/unit_tests/rules/traits.rs @@ -1,11 +1,6 @@ -#[test] -fn test_traits_compile() { - // Traits should compile - actual tests in reduction implementations -} - use crate::rules::traits::{ - validate_target_solution, AggregateReductionResult, DynAggregateReductionResult, ReduceTo, - ReduceToAggregate, ReductionResult, + AggregateReductionResult, DynAggregateReductionResult, ReduceTo, ReduceToAggregate, + ReductionResult, }; use crate::traits::Problem; use crate::types::Sum; @@ -47,37 +42,28 @@ impl Problem for SourceProblem { } } -impl crate::solvers::BruteForceProblem for SourceProblem { - fn dimensions(&self) -> Vec { - vec![2, 2] - } -} - impl Problem for TargetProblem { const NAME: &'static str = "Target"; type Solution = Vec; - type Value = i64; + type Value = crate::types::Max; crate::problem_parameters![("num_variables", num_variables)]; - fn evaluate(&self, config: &Self::Solution) -> Result { + fn evaluate( + &self, + config: &Self::Solution, + ) -> Result { if config.len() != 2 || config.iter().any(|&value| value >= 2) { return Err(crate::traits::EvaluationError::InvalidConfiguration( "expected two binary target values".to_string(), )); } - Ok((config[0] + config[1]) as i64) + Ok(crate::types::Max(Some((config[0] + config[1]) as i64))) } fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "i64")] } } -impl crate::solvers::BruteForceProblem for TargetProblem { - fn dimensions(&self) -> Vec { - vec![2, 2] - } -} - #[derive(Clone)] struct TestReduction { target: TargetProblem, @@ -112,43 +98,43 @@ fn test_reduction() { let result = >::reduce_to(&source) .expect("reduction should succeed"); let target = result.target_problem(); - assert_eq!(target.evaluate(&vec![1, 1]).unwrap(), 2); + assert_eq!( + target.evaluate(&vec![1, 1]).unwrap(), + crate::types::Max(Some(2)) + ); assert_eq!(result.extract_solution(&vec![1, 0]).unwrap(), vec![1, 0]); } -#[test] -fn target_solution_validation_rejects_shape_and_domain_errors() { - let target = TargetProblem; - - assert_eq!(validate_target_solution(&target, &vec![1, 0]).unwrap(), 1); - assert!(validate_target_solution(&target, &vec![1]).is_err()); - assert!(validate_target_solution(&target, &vec![1, 0, 0]).is_err()); - assert!(validate_target_solution(&target, &vec![1, 2]).is_err()); -} - #[test] fn aggregate_value_from_solution_keeps_evaluation_errors_distinct_from_false() { use crate::models::decision::Decision; use crate::models::graph::MinimumVertexCover; use crate::rules::ExtractionError; use crate::topology::SimpleGraph; - use crate::types::Or; let source = Decision::new( - MinimumVertexCover::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i64; 2]), + MinimumVertexCover::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![1i64; 2]).unwrap(), 0, ); - let reduction = source.reduce_to_aggregate().unwrap(); - let value = reduction - .extract_value_from_solution_dyn(&vec![true, false]) + let edge = crate::rules::registry::reduction_entries() + .into_iter() + .find(|edge| { + edge.source_name == "DecisionMinimumVertexCover" + && edge.target_name == "MinimumVertexCover" + && (edge.source_variant_fn)() + == > as Problem>::variant() + }) .unwrap(); - assert_eq!(value.downcast_ref::(), Some(&Or(false))); + let step = (edge.reduce_fn.unwrap())(&source).unwrap(); + let interpret = step.interpret_optimum.as_ref().unwrap(); + let value = interpret(&vec![true, false]).unwrap(); + assert!(!value); assert!(matches!( - reduction.extract_value_from_solution_dyn(&vec![true]), + interpret(&vec![true]), Err(ExtractionError::Evaluation(_)) )); assert!(matches!( - reduction.extract_value_from_solution_dyn(&vec![1i64, 0]), + interpret(&vec![1i64, 0]), Err(ExtractionError::InvalidTargetSolution(_)) )); } @@ -190,12 +176,6 @@ impl Problem for AggregateSourceProblem { } } -impl crate::solvers::BruteForceProblem for AggregateSourceProblem { - fn dimensions(&self) -> Vec { - vec![2] - } -} - impl Problem for AggregateTargetProblem { const NAME: &'static str = "AggregateTarget"; type Solution = Vec; @@ -215,12 +195,6 @@ impl Problem for AggregateTargetProblem { } } -impl crate::solvers::BruteForceProblem for AggregateTargetProblem { - fn dimensions(&self) -> Vec { - vec![2] - } -} - struct TestAggregateReduction { target: AggregateTargetProblem, offset: u64, diff --git a/src/unit_tests/rules/travelingsalesman_ilp.rs b/src/unit_tests/rules/travelingsalesman_ilp.rs index d7c630715..18905a170 100644 --- a/src/unit_tests/rules/travelingsalesman_ilp.rs +++ b/src/unit_tests/rules/travelingsalesman_ilp.rs @@ -6,18 +6,18 @@ use crate::types::Min; fn k4_tsp() -> TravelingSalesman { TravelingSalesman::new( - SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(), vec![10, 15, 20, 35, 25, 30], ) + .unwrap() } #[test] fn test_reduction_creates_valid_ilp_c4() { // C4 cycle: 4 vertices, 4 edges. Unique Hamiltonian cycle (the cycle itself). - let problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 4, - vec![(0, 1), (1, 2), (2, 3), (3, 0)], - )); + let problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(), + ); let reduction: ReductionTSPToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -30,10 +30,9 @@ fn test_reduction_creates_valid_ilp_c4() { #[test] fn test_reduction_c4_closed_loop() { // C4 cycle with unit weights: optimal tour cost = 4 - let problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 4, - vec![(0, 1), (1, 2), (2, 3), (3, 0)], - )); + let problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(), + ); let reduction: ReductionTSPToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -77,10 +76,9 @@ fn test_reduction_k4_weighted_closed_loop() { #[test] fn test_reduction_c5_unweighted_closed_loop() { // C5 cycle with unit weights - let problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 5, - vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], - )); + let problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(), + ); let reduction: ReductionTSPToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -97,10 +95,9 @@ fn test_reduction_c5_unweighted_closed_loop() { #[test] fn test_no_hamiltonian_cycle_infeasible() { // Path graph 0-1-2-3: no Hamiltonian cycle exists - let problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 4, - vec![(0, 1), (1, 2), (2, 3)], - )); + let problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), + ); let reduction: ReductionTSPToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -108,8 +105,9 @@ fn test_no_hamiltonian_cycle_infeasible() { let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(ilp); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Path graph should have no Hamiltonian cycle (infeasible ILP)" ); } @@ -117,10 +115,9 @@ fn test_no_hamiltonian_cycle_infeasible() { #[test] fn test_solution_extraction_structure() { // C4 cycle: verify extraction produces correct edge selection format - let problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 4, - vec![(0, 1), (1, 2), (2, 3), (3, 0)], - )); + let problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(), + ); let reduction: ReductionTSPToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -155,10 +152,9 @@ fn test_solve_via_ilp_pipeline() { #[test] fn test_travelingsalesman_to_ilp_bf_vs_ilp() { - let problem = TravelingSalesman::<_, i64>::unit_weights(SimpleGraph::new( - 4, - vec![(0, 1), (1, 2), (2, 3), (3, 0)], - )); + let problem = TravelingSalesman::<_, i64>::unit_weights( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(), + ); let reduction: ReductionTSPToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); diff --git a/src/unit_tests/rules/travelingsalesman_qubo.rs b/src/unit_tests/rules/travelingsalesman_qubo.rs index 77199d7e3..ac5fcbcdf 100644 --- a/src/unit_tests/rules/travelingsalesman_qubo.rs +++ b/src/unit_tests/rules/travelingsalesman_qubo.rs @@ -7,8 +7,8 @@ use crate::types::Min; #[test] fn test_travelingsalesman_to_qubo_closed_loop() { // K3 complete graph with weights [1, 2, 3] - let graph = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); - let tsp = TravelingSalesman::new(graph, vec![1i64, 2, 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); + let tsp = TravelingSalesman::new(graph, vec![1i64, 2, 3]).unwrap(); let reduction = ReduceTo::>::reduce_to(&tsp).expect("reduction should succeed"); let qubo = reduction.target_problem(); @@ -35,8 +35,8 @@ fn test_travelingsalesman_to_qubo_closed_loop() { #[test] fn test_travelingsalesman_to_qubo_k4() { // K4 with unit weights - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); - let tsp = TravelingSalesman::new(graph, vec![1i64; 6]); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); + let tsp = TravelingSalesman::new(graph, vec![1i64; 6]).unwrap(); let reduction = ReduceTo::>::reduce_to(&tsp).expect("reduction should succeed"); let qubo = reduction.target_problem(); @@ -63,22 +63,22 @@ fn test_travelingsalesman_to_qubo_k4() { #[test] fn test_travelingsalesman_to_qubo_sizes() { // K3: n=3, QUBO should have n^2 = 9 variables - let graph3 = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); - let tsp3 = TravelingSalesman::new(graph3, vec![1i64; 3]); + let graph3 = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); + let tsp3 = TravelingSalesman::new(graph3, vec![1i64; 3]).unwrap(); let reduction3 = ReduceTo::>::reduce_to(&tsp3).expect("reduction should succeed"); - assert_eq!(reduction3.target_problem().num_variables(), 9); + assert_eq!(reduction3.target_problem().num_variables().unwrap(), 9); // K4: n=4, QUBO should have n^2 = 16 variables - let graph4 = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); - let tsp4 = TravelingSalesman::new(graph4, vec![1i64; 6]); + let graph4 = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).unwrap(); + let tsp4 = TravelingSalesman::new(graph4, vec![1i64; 6]).unwrap(); let reduction4 = ReduceTo::>::reduce_to(&tsp4).expect("reduction should succeed"); - assert_eq!(reduction4.target_problem().num_variables(), 16); + assert_eq!(reduction4.target_problem().num_variables().unwrap(), 16); } #[test] fn test_travelingsalesman_to_qubo_weighted_corpus_regression() { // Unequal tour costs expose a transposed vertex/position permutation. - let tsp = TravelingSalesman::new(SimpleGraph::complete(4), vec![9i64, 1, 2, 3, 4, 8]); + let tsp = TravelingSalesman::new(SimpleGraph::complete(4), vec![9i64, 1, 2, 3, 4, 8]).unwrap(); let reduction = ReduceTo::>::reduce_to(&tsp).unwrap(); crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target( &tsp, @@ -86,3 +86,84 @@ fn test_travelingsalesman_to_qubo_weighted_corpus_regression() { "weighted TSP position encoding", ); } + +#[test] +fn signed_and_small_tours_recover_all_optima_or_infeasibility() { + let cases = [ + (0, vec![], vec![]), + (1, vec![], vec![]), + (1, vec![(0, 0), (0, 0)], vec![4, -2]), + (2, vec![(0, 1)], vec![1]), + (2, vec![(0, 1), (0, 1), (0, 1)], vec![4, -2, 1]), + (3, vec![(0, 1), (1, 2)], vec![-5, 2]), + (3, vec![(0, 1), (1, 2), (0, 2)], vec![-5, 2, 1]), + ( + 3, + vec![(0, 1), (0, 1), (1, 2), (0, 2), (1, 1)], + vec![4, -5, 2, 1, -100], + ), + ( + 4, + vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], + vec![-9, 1, 2, 3, 4, -8], + ), + ]; + for (n, edges, weights) in cases { + let m = edges.len(); + let source = TravelingSalesman::new(SimpleGraph::new(n, edges).unwrap(), weights).unwrap(); + let expected = (0..1usize << m) + .filter_map(|bits| { + source + .evaluate(&(0..m).map(|i| bits & (1 << i) != 0).collect()) + .unwrap() + .0 + }) + .min(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let entry = inventory::iter:: + .into_iter() + .find(|entry| entry.source_name == "TravelingSalesman" && entry.target_name == "QUBO") + .unwrap(); + let chain = + crate::rules::ReductionChain::execute(&source, &[entry.reduce_fn.unwrap()]).unwrap(); + + let solutions = BruteForce::new() + .find_all_witnesses(reduction.target_problem()) + .unwrap(); + assert!(!solutions.is_empty()); + for solution in solutions { + let completed = crate::solvers::complete_reduction( + &source, + &chain, + &crate::solvers::SolveOutcome::Optimal { + solution: serde_json::to_value(&solution).unwrap(), + evaluation: String::new(), + }, + ) + .unwrap(); + assert_eq!( + matches!(completed, crate::solvers::SolveOutcome::Optimal { .. }), + expected.is_some() + ); + assert_eq!( + crate::rules::AggregateReductionResult::extract_value( + &reduction, + reduction.target_problem().evaluate(&solution).unwrap() + ), + Min(expected) + ); + if expected.is_some() { + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Min(expected) + ); + } + } + assert_eq!( + crate::rules::AggregateReductionResult::extract_value(&reduction, Min(None)), + Min(None) + ); + } +} diff --git a/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs b/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs index feb60cb9b..9010944f9 100644 --- a/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs @@ -8,26 +8,28 @@ fn feasible_instance() -> UndirectedFlowLowerBounds { // 3-vertex path: edges (0,1) cap=2 lower=1, (1,2) cap=2 lower=1 // source=0, sink=2, requirement=1 UndirectedFlowLowerBounds::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![2, 2], vec![1, 1], 0, 2, 1, ) + .unwrap() } fn infeasible_instance() -> UndirectedFlowLowerBounds { // 3-vertex path: edges (0,1) cap=2 lower=2, (1,2) cap=1 lower=0 // source=0, sink=2, requirement=2: need 2 units but edge (1,2) cap=1 limits to 1 UndirectedFlowLowerBounds::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![2, 1], vec![0, 0], 0, 2, 2, ) + .unwrap() } #[test] @@ -76,8 +78,9 @@ fn test_undirectedflowlowerbounds_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionUFLBToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs index 1d8d087ed..0bf3adc13 100644 --- a/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -8,7 +8,7 @@ fn feasible_instance() -> UndirectedTwoCommodityIntegralFlow { // 4-vertex graph: edges (0,2),(1,2),(2,3); capacities [1,1,2] // s1=0, t1=3, s2=1, t2=3, R1=1, R2=1 UndirectedTwoCommodityIntegralFlow::new( - SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]).unwrap(), vec![1, 1, 2], 0, 3, @@ -17,6 +17,7 @@ fn feasible_instance() -> UndirectedTwoCommodityIntegralFlow { 1, 1, ) + .unwrap() } fn infeasible_instance() -> UndirectedTwoCommodityIntegralFlow { @@ -24,7 +25,7 @@ fn infeasible_instance() -> UndirectedTwoCommodityIntegralFlow { // path graph: 0-1-2; cap=1 everywhere; s1=0,t1=2 req=1; s2=0,t2=2 req=1 // Total demand = 2 on edge (0,1) but cap = 1 → infeasible UndirectedTwoCommodityIntegralFlow::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1, 1], 0, 2, @@ -33,6 +34,7 @@ fn infeasible_instance() -> UndirectedTwoCommodityIntegralFlow { 1, 1, ) + .unwrap() } #[test] @@ -116,8 +118,9 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionU2CIFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible flow instance should yield infeasible ILP" ); } @@ -125,7 +128,7 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_infeasible() { #[test] fn test_other_commodity_source_cannot_create_flow() { let problem = UndirectedTwoCommodityIntegralFlow::new( - SimpleGraph::new(4, vec![(1, 3)]), + SimpleGraph::new(4, vec![(1, 3)]).unwrap(), vec![2], 0, 3, @@ -133,10 +136,14 @@ fn test_other_commodity_source_cannot_create_flow() { 3, 1, 0, - ); + ) + .unwrap(); let reduction: ReductionU2CIFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/solvers/brute_force.rs b/src/unit_tests/solvers/brute_force.rs index f2b6547d7..38ca74e72 100644 --- a/src/unit_tests/solvers/brute_force.rs +++ b/src/unit_tests/solvers/brute_force.rs @@ -14,7 +14,12 @@ impl Problem for MaxSumProblem { type Solution = Vec; type Value = Max; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.weights.len() as u64)]) + } fn evaluate( &self, @@ -37,8 +42,12 @@ impl Problem for MaxSumProblem { } impl crate::solvers::BruteForceProblem for MaxSumProblem { - fn dimensions(&self) -> Vec { - vec![2; self.weights.len()] + fn num_variables(&self) -> Result { + Ok(self.weights.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -52,7 +61,12 @@ impl Problem for MinSumProblem { type Solution = Vec; type Value = Min; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.weights.len() as u64)]) + } fn evaluate( &self, @@ -75,8 +89,12 @@ impl Problem for MinSumProblem { } impl crate::solvers::BruteForceProblem for MinSumProblem { - fn dimensions(&self) -> Vec { - vec![2; self.weights.len()] + fn num_variables(&self) -> Result { + Ok(self.weights.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -91,7 +109,12 @@ impl Problem for SatProblem { type Solution = Vec; type Value = Or; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.num_vars as u64)]) + } fn evaluate( &self, @@ -106,8 +129,12 @@ impl Problem for SatProblem { } impl crate::solvers::BruteForceProblem for SatProblem { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -119,7 +146,12 @@ impl Problem for EvaluationFailureProblem { type Solution = Vec; type Value = Or; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", 1usize as u64)]) + } fn evaluate(&self, config: &Self::Solution) -> Result { if config.as_slice() == [1] { @@ -137,8 +169,12 @@ impl Problem for EvaluationFailureProblem { } impl crate::solvers::BruteForceProblem for EvaluationFailureProblem { - fn dimensions(&self) -> Vec { - vec![2] + fn num_variables(&self) -> Result { + Ok(1usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([2][variable]) } } @@ -150,7 +186,12 @@ impl Problem for AggregationFailureProblem { type Solution = Vec; type Value = Max; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", 1usize as u64)]) + } fn evaluate(&self, _: &Self::Solution) -> Result, crate::traits::EvaluationError> { Ok(Max(Some(f64::NAN))) @@ -162,8 +203,12 @@ impl Problem for AggregationFailureProblem { } impl crate::solvers::BruteForceProblem for AggregationFailureProblem { - fn dimensions(&self) -> Vec { - vec![2] + fn num_variables(&self) -> Result { + Ok(1usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([2][variable]) } } @@ -178,7 +223,12 @@ impl Problem for CountingSatProblem { type Solution = Vec; type Value = Or; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", 2usize as u64)]) + } fn evaluate( &self, @@ -196,8 +246,12 @@ impl Problem for CountingSatProblem { } impl crate::solvers::BruteForceProblem for CountingSatProblem { - fn dimensions(&self) -> Vec { - vec![2, 2] + fn num_variables(&self) -> Result { + Ok(2usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([2, 2][variable]) } } @@ -427,9 +481,10 @@ fn test_solver_with_real_mis() { use crate::traits::Problem; let problem = MaximumIndependentSet::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); let solver = BruteForce::new(); let best = solver.find_all_witnesses(&problem).unwrap(); @@ -563,18 +618,309 @@ fn cartesian_indices_zero_dimension_has_no_candidates() { } #[test] -fn cartesian_indices_is_exact_size() { +fn cartesian_indices_reports_exhaustion_without_a_total_count() { let mut indices = CartesianIndices::new(vec![2, 3]).unwrap(); - assert_eq!(indices.len(), 6); - indices.next(); - assert_eq!(indices.len(), 5); + assert_eq!(indices.size_hint(), (1, None)); + assert_eq!(indices.by_ref().count(), 6); + assert_eq!(indices.size_hint(), (0, Some(0))); + assert_eq!(indices.next(), None); +} + +#[test] +fn cartesian_indices_visits_a_prefix_when_the_total_exceeds_usize() { + let prefix = CartesianIndices::new(vec![usize::MAX, 2]) + .unwrap() + .take(4) + .collect::>(); + assert_eq!(prefix, vec![vec![0, 0], vec![0, 1], vec![1, 0], vec![1, 1]]); +} + +#[test] +fn enumeration_reports_coordinate_count_and_storage_errors() { + use crate::models::set::SetBasis; + let count_overflow = SetBasis::new(2, vec![], usize::MAX).unwrap(); + assert!(matches!( + BruteForceProblem::num_variables(&count_overflow), + Err(SolveError::IntegerOverflow(_)) + )); + assert!(matches!( + BruteForce::new().solve(&count_overflow), + Err(SolveError::IntegerOverflow(_)) + )); + let allocation_overflow = SetBasis::new(1, vec![], usize::MAX).unwrap(); + assert!(matches!( + cartesian_dimensions(&allocation_overflow), + Err(SolveError::Allocation(_)) + )); +} + +#[test] +fn window_product_does_not_restrict_construction_or_evaluation() { + use crate::models::misc::ClosestSubstring; + let problem = ClosestSubstring::new(1, vec![vec![0, 0]; 64], 1).unwrap(); + let restored: ClosestSubstring = + serde_json::from_value(serde_json::to_value(&problem).unwrap()).unwrap(); + assert_eq!(restored.evaluate(&vec![0; 65]).unwrap(), Min(Some(0))); + assert_eq!(restored.parameters(), problem.parameters()); + let dimensions = crate::solvers::cartesian_dimensions(&restored).unwrap(); + assert_eq!(dimensions[0], 1); + assert_eq!(&dimensions[1..], &[2; 64]); + let prefix: Vec<_> = CartesianIndices::new(dimensions).unwrap().take(2).collect(); + assert_eq!(prefix.len(), 2); + for witness in prefix { + assert_eq!(restored.evaluate(&witness).unwrap(), Min(Some(0))); + } +} + +#[test] +fn scalar_counts_report_unrepresentable_search_coordinates() { + use crate::models::algebraic::BMF; + use crate::models::misc::{ConsistencyOfDatabaseFrequencyTables, EnsembleComputation}; + let cases = [ + ( + "biclique slots", + crate::models::graph::BicliqueCover::new( + crate::topology::BipartiteGraph::new(1, 1, vec![(0, 0)]).unwrap(), + usize::MAX, + ) + .num_variables(), + ), + ( + "tree slots", + crate::models::graph::KthBestSpanningTree::new( + crate::topology::SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1i64, 1], + usize::MAX, + 2, + ) + .unwrap() + .num_variables(), + ), + ( + "tile slots", + crate::models::misc::SquareTiling::new(1, vec![(0, 0, 0, 0)], usize::MAX) + .num_variables(), + ), + ( + "factor rows", + BMF::new(vec![vec![true]; 2], usize::MAX) + .unwrap() + .num_variables(), + ), + ( + "factor columns", + BMF::new(vec![vec![true; 2]], usize::MAX) + .unwrap() + .num_variables(), + ), + ( + "factor sum", + BMF::new(vec![vec![true]], usize::MAX) + .unwrap() + .num_variables(), + ), + ( + "operation operands", + EnsembleComputation::new(1, vec![], usize::MAX).num_variables(), + ), + ( + "database entries", + ConsistencyOfDatabaseFrequencyTables::new(usize::MAX, vec![1, 1], vec![], vec![]) + .unwrap() + .num_variables(), + ), + ]; + for (context, result) in cases { + assert!( + matches!(result, Err(SolveError::IntegerOverflow(_))), + "{context}: {result:?}" + ); + } } #[test] -fn cartesian_indices_reports_cardinality_overflow() { +fn scalar_domains_report_unrepresentable_coordinate_cardinalities() { + use crate::models::misc::{ + ConjunctiveQueryFoldability, EnsembleComputation, MinimumExternalMacroDataCompression, + MinimumInternalMacroDataCompression, + }; + let external = MinimumExternalMacroDataCompression::new(usize::MAX, vec![0], 1).unwrap(); + let cases = [ + ("external symbol", external.dimension(0)), + ("external pointer", external.dimension(1)), + ( + "internal alphabet", + MinimumInternalMacroDataCompression::new(usize::MAX, vec![0], 1) + .unwrap() + .dimension(0), + ), + ( + "internal sentinel", + MinimumInternalMacroDataCompression::new(usize::MAX - 1, vec![0], 1) + .unwrap() + .dimension(0), + ), + ( + "operand labels", + EnsembleComputation::new(usize::MAX, vec![], 1).dimension(0), + ), + ( + "distinguished labels", + ConjunctiveQueryFoldability::new(usize::MAX, 1, 1, vec![], vec![], vec![]) + .unwrap() + .dimension(0), + ), + ( + "undistinguished labels", + ConjunctiveQueryFoldability::new(usize::MAX, 0, 1, vec![], vec![], vec![]) + .unwrap() + .dimension(0), + ), + ]; + for (context, result) in cases { + assert!( + matches!(result, Err(SolveError::IntegerOverflow(_))), + "{context}: {result:?}" + ); + } +} + +#[test] +fn string_domains_reserve_a_representable_sentinel() { + use crate::models::misc::{ + LongestCommonSubsequence, ShortestCommonSupersequence, ShortestCommonSuperstring, + }; + use crate::models::set::ConsecutiveSets; + let cases = [ + ( + "subsequence", + LongestCommonSubsequence::new(usize::MAX, vec![vec![0]]) + .unwrap() + .dimension(0), + ), + ( + "supersequence", + ShortestCommonSupersequence::new(usize::MAX, vec![vec![0]]) + .unwrap() + .dimension(0), + ), + ( + "superstring", + ShortestCommonSuperstring::new(usize::MAX, vec![vec![0]]) + .unwrap() + .dimension(0), + ), + ( + "consecutive sets", + ConsecutiveSets::new(usize::MAX, vec![vec![0]], 1) + .unwrap() + .dimension(0), + ), + ]; + for (context, result) in cases { + assert!( + matches!(result, Err(SolveError::IntegerOverflow(_))), + "{context}: {result:?}" + ); + } +} + +#[test] +fn decision_tree_slots_fail_before_enumeration_storage_is_allocated() { + use crate::models::misc::MinimumDecisionTree; + let objects = usize::BITS as usize + 1; + let tests = objects.ilog2() as usize + 1; + let matrix = (0..tests) + .map(|bit| { + (0..objects) + .map(|object| object & (1 << bit) != 0) + .collect() + }) + .collect(); + let problem = MinimumDecisionTree::new(matrix, objects, tests).unwrap(); assert!(matches!( - CartesianIndices::new(vec![usize::MAX, 2]), - Err(crate::solvers::SolveError::SearchSpaceOverflow(dimensions)) - if dimensions == vec![usize::MAX, 2] + cartesian_dimensions(&problem), + Err(SolveError::Evaluation( + crate::traits::EvaluationError::IntegerOverflow(_) + )) + )); +} + +#[test] +fn large_products_remain_symbolic_in_model_parameters() { + use crate::models::misc::{ + ConsistencyOfDatabaseFrequencyTables, MinimumDiscretePlanarInverseKinematics, + }; + let arm = MinimumDiscretePlanarInverseKinematics::new( + vec![1.0; 64], + (64.0, 0.0), + vec![vec![0.0, 1.0]; 64], + vec![vec![(0, 0), (0, 1), (1, 0), (1, 1)]; 63], + ) + .unwrap(); + let restored: MinimumDiscretePlanarInverseKinematics = + serde_json::from_value(serde_json::to_value(&arm).unwrap()).unwrap(); + assert_eq!(arm.parameters(), restored.parameters()); + assert_eq!(arm.evaluate(&vec![0; 64]).unwrap(), Min(Some(0.0))); + assert_eq!( + CartesianIndices::new(cartesian_dimensions(&arm).unwrap()) + .unwrap() + .take(2) + .count(), + 2 + ); + let database = + ConsistencyOfDatabaseFrequencyTables::new(1, vec![2; 64], vec![], vec![]).unwrap(); + let restored: ConsistencyOfDatabaseFrequencyTables = + serde_json::from_value(serde_json::to_value(&database).unwrap()).unwrap(); + assert_eq!(database.parameters(), restored.parameters()); + assert_eq!(database.evaluate(&vec![0; 64]).unwrap(), Or(true)); +} + +#[test] +fn test_max_solution_selection() { + assert!(Max::contributes_to_solution(&Max(Some(7)), &Max(Some(7)))); + assert!(!Max::contributes_to_solution(&Max(Some(3)), &Max(Some(7)))); + assert!(!Max::contributes_to_solution(&Max(None), &Max(Some(7)))); +} + +#[test] +fn test_min_solution_selection() { + assert!(Min::contributes_to_solution(&Min(Some(3)), &Min(Some(3)))); + assert!(!Min::contributes_to_solution(&Min(Some(7)), &Min(Some(3)))); + assert!(!Min::contributes_to_solution(&Min(None), &Min(Some(3)))); +} + +#[test] +fn test_or_solution_selection() { + assert!(Or::contributes_to_solution(&Or(true), &Or(true))); + assert!(!Or::contributes_to_solution(&Or(false), &Or(true))); + assert!(!Or::contributes_to_solution(&Or(true), &Or(false))); +} + +#[test] +fn test_extremum_solution_selection() { + // Matching value and sense -> contributes + assert!(Extremum::contributes_to_solution( + &Extremum::maximize(Some(10)), + &Extremum::maximize(Some(10)), + )); + + // Different value -> does not contribute + assert!(!Extremum::contributes_to_solution( + &Extremum::maximize(Some(5)), + &Extremum::maximize(Some(10)), )); + + // None config -> does not contribute + assert!(!Extremum::contributes_to_solution( + &Extremum::::maximize(None), + &Extremum::maximize(Some(10)), + )); +} + +#[test] +fn test_minimumcutintoboundedsets_selects_optimal_solutions() { + type Value = as Problem>::Value; + assert!(Value::contributes_to_solution(&Min(Some(3)), &Min(Some(3)))); } diff --git a/src/unit_tests/solvers/customized/closest_vector_problem.rs b/src/unit_tests/solvers/customized/closest_vector_problem.rs index fe570f3f2..240a43792 100644 --- a/src/unit_tests/solvers/customized/closest_vector_problem.rs +++ b/src/unit_tests/solvers/customized/closest_vector_problem.rs @@ -31,31 +31,12 @@ fn test_cvp_solver_keeps_zero_on_tie_and_handles_empty_basis() { } #[test] -fn test_cvp_solver_reports_inexact_integer_conversion() { - let problem = ClosestVectorProblem::new( - vec![vec![crate::types::MAX_EXACT_F64_INTEGER + 1]], - vec![0_i64], - ) - .unwrap(); - assert!(matches!( - solve(&problem), - Err(crate::solvers::SolveError::InexactFloatConversion(_)) - )); - +fn test_cvp_solver_reports_search_representation_overflow() { let out_of_range = ClosestVectorProblem::new(vec![vec![1]], vec![1e20]).unwrap(); assert!(matches!( solve(&out_of_range), Err(SolveError::IntegerOverflow(_)) )); - let inexact = ClosestVectorProblem::new( - vec![vec![1]], - vec![crate::types::MAX_EXACT_F64_INTEGER as f64 + 2.0], - ) - .unwrap(); - assert!(matches!( - solve(&inexact), - Err(SolveError::InexactFloatConversion(_)) - )); } #[test] @@ -141,7 +122,10 @@ fn test_cvp_pruning_preserves_exact_large_translation_optimum() { let expected = vec![coefficient, coefficient]; assert_eq!(solve(&integer).unwrap(), expected); assert_eq!(solve(&real).unwrap(), expected); - assert_eq!(integer.evaluate(&expected).unwrap().0, Some(0.0)); + assert_eq!( + integer.evaluate(&expected).unwrap().0, + Some(BigRational::zero()) + ); } } diff --git a/src/unit_tests/solvers/customized/grouping_by_swapping.rs b/src/unit_tests/solvers/customized/grouping_by_swapping.rs index e3cdfebf4..aa69b1671 100644 --- a/src/unit_tests/solvers/customized/grouping_by_swapping.rs +++ b/src/unit_tests/solvers/customized/grouping_by_swapping.rs @@ -16,7 +16,8 @@ fn test_symbol_block_order_grouping_by_swapping_matches_brute_force() { value /= alphabet_size; } for budget in 0..=4 { - let problem = GroupingBySwapping::new(alphabet_size, string.clone(), budget); + let problem = + GroupingBySwapping::new(alphabet_size, string.clone(), budget).unwrap(); let expected = BruteForce::new().solve(&problem).unwrap().is_some(); let actual = solve(&problem); assert_eq!(actual.is_some(), expected, "{string:?}, budget={budget}"); @@ -32,11 +33,11 @@ fn test_symbol_block_order_grouping_by_swapping_matches_brute_force() { #[test] fn test_symbol_block_order_grouping_by_swapping_handles_scale() { assert_eq!( - solve(&GroupingBySwapping::new(0, Vec::new(), 0)), + solve(&GroupingBySwapping::new(0, Vec::new(), 0).unwrap()), Some(Vec::new()) ); - let problem = GroupingBySwapping::new(4, vec![2, 1, 1, 1, 0, 0, 3, 2], 24); + let problem = GroupingBySwapping::new(4, vec![2, 1, 1, 1, 0, 0, 3, 2], 24).unwrap(); let solution = solve(&problem).expect("the instance is groupable within its budget"); assert_eq!(solution.len(), 24); assert_eq!(problem.evaluate(&solution).unwrap(), Or(true)); diff --git a/src/unit_tests/solvers/customized/minimum_cost_circulation.rs b/src/unit_tests/solvers/customized/minimum_cost_circulation.rs index 7e51c8e35..79e224ef2 100644 --- a/src/unit_tests/solvers/customized/minimum_cost_circulation.rs +++ b/src/unit_tests/solvers/customized/minimum_cost_circulation.rs @@ -5,7 +5,7 @@ use crate::traits::Problem; #[test] fn test_negative_cycle_cancellation_minimum_cost_circulation_matches_brute_force() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0), (1, 0)]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0), (1, 0)]).unwrap(); for capacity_mask in 0usize..16 { let capacities = (0..4) .map(|arc| ((capacity_mask >> arc) & 1) as i64) @@ -19,7 +19,8 @@ fn test_negative_cycle_cancellation_minimum_cost_circulation_matches_brute_force cost }) .collect::>(); - let problem = MinimumCostCirculation::new(graph.clone(), capacities.clone(), costs); + let problem = + MinimumCostCirculation::new(graph.clone(), capacities.clone(), costs).unwrap(); let expected = BruteForce::new().solve(&problem).unwrap().unwrap(); let actual = solve(&problem).unwrap(); assert_eq!( @@ -33,10 +34,11 @@ fn test_negative_cycle_cancellation_minimum_cost_circulation_matches_brute_force #[test] fn test_negative_cycle_cancellation_minimum_cost_circulation_handles_multigraph() { let problem = MinimumCostCirculation::new( - DirectedGraph::new(2, vec![(0, 0), (0, 1), (0, 1), (1, 0)]), + DirectedGraph::new(2, vec![(0, 0), (0, 1), (0, 1), (1, 0)]).unwrap(), vec![3, 2, 4, 3], vec![-2, 4, -3, 1], - ); + ) + .unwrap(); let solution = solve(&problem).unwrap(); assert_eq!(problem.evaluate(&solution).unwrap().0, Some(-12)); } @@ -45,17 +47,17 @@ fn test_negative_cycle_cancellation_minimum_cost_circulation_handles_multigraph( fn test_circulation_overflow_propagates_through_default_solver() { for (graph, costs, operation) in [ ( - DirectedGraph::new(2, vec![(0, 1), (1, 0)]), + DirectedGraph::new(2, vec![(0, 1), (1, 0)]).unwrap(), vec![-5_000_000_000_000_000_000, 0], "relaxing a circulation residual arc", ), ( - DirectedGraph::new(1, vec![(0, 0)]), + DirectedGraph::new(1, vec![(0, 0)]).unwrap(), vec![i64::MIN], "negating a circulation residual cost", ), ] { - let problem = MinimumCostCirculation::new(graph, vec![1; costs.len()], costs); + let problem = MinimumCostCirculation::new(graph, vec![1; costs.len()], costs).unwrap(); let reference = BruteForce::new().solve(&problem).unwrap().unwrap(); assert!(problem.evaluate(&reference).unwrap().is_valid()); let loaded = crate::registry::load_dyn( diff --git a/src/unit_tests/solvers/customized/minimum_decision_tree.rs b/src/unit_tests/solvers/customized/minimum_decision_tree.rs index 6905c543e..f268b596b 100644 --- a/src/unit_tests/solvers/customized/minimum_decision_tree.rs +++ b/src/unit_tests/solvers/customized/minimum_decision_tree.rs @@ -1,5 +1,6 @@ use super::*; -use crate::solvers::BruteForce; +use crate::registry::load_dyn; +use crate::solvers::{BruteForce, SolveOutcome, SolverExecution, SolverRequest}; use crate::traits::Problem; #[test] @@ -25,7 +26,7 @@ fn test_subset_dp_minimum_decision_tree_matches_brute_force() { }) { continue; } - let problem = MinimumDecisionTree::new(matrix, 3, 3); + let problem = MinimumDecisionTree::new(matrix, 3, 3).unwrap(); let expected = BruteForce::new().solve(&problem).unwrap().unwrap(); let actual = solve(&problem).unwrap(); assert_eq!( @@ -42,7 +43,51 @@ fn test_subset_dp_minimum_decision_tree_handles_eight_objects() { let matrix = (0..3) .map(|bit| (0..8).map(|object| object & (1 << bit) != 0).collect()) .collect(); - let problem = MinimumDecisionTree::new(matrix, 8, 3); - let solution = solve(&problem).unwrap(); + let problem = MinimumDecisionTree::new(matrix, 8, 3).unwrap(); + let loaded = load_dyn( + MinimumDecisionTree::NAME, + &Default::default(), + serde_json::to_value(&problem).unwrap(), + ) + .unwrap(); + let result = crate::solvers::solve(&loaded, SolverRequest::Default).unwrap(); + assert!(matches!( + result.solver, + SolverExecution::Customized { + implementation: "subset-dp" + } + )); + let SolveOutcome::Optimal { + solution, + evaluation, + } = result.outcome + else { + panic!("the instance has a solution"); + }; + assert_eq!(evaluation, "Min(24)"); + let solution = serde_json::from_value(solution).unwrap(); assert_eq!(problem.evaluate(&solution).unwrap().0, Some(24)); } + +#[test] +fn subset_dp_reports_mask_and_table_representation_errors() { + for n in [usize::BITS as usize, usize::BITS as usize - 1] { + let tests = (n.ilog2() + 1) as usize; + let matrix = (0..tests) + .map(|bit| (0..n).map(|object| object & (1 << bit) != 0).collect()) + .collect(); + let problem = MinimumDecisionTree::new(matrix, n, tests).unwrap(); + let loaded = load_dyn( + MinimumDecisionTree::NAME, + &Default::default(), + serde_json::to_value(&problem).unwrap(), + ) + .unwrap(); + let error = crate::solvers::solve(&loaded, SolverRequest::Default).unwrap_err(); + if n == usize::BITS as usize { + assert!(matches!(error, SolveError::IntegerOverflow(_))); + } else { + assert!(matches!(error, SolveError::Allocation(_))); + } + } +} diff --git a/src/unit_tests/solvers/customized/minimum_intersection_graph_basis.rs b/src/unit_tests/solvers/customized/minimum_intersection_graph_basis.rs index 74335d39c..3f6906983 100644 --- a/src/unit_tests/solvers/customized/minimum_intersection_graph_basis.rs +++ b/src/unit_tests/solvers/customized/minimum_intersection_graph_basis.rs @@ -11,7 +11,7 @@ fn test_clique_cover_dp_minimum_intersection_graph_basis_matches_brute_force() { .enumerate() .filter_map(|(edge, pair)| (edge_mask & (1 << edge) != 0).then_some(*pair)) .collect(); - let problem = MinimumIntersectionGraphBasis::new(SimpleGraph::new(3, edges)); + let problem = MinimumIntersectionGraphBasis::new(SimpleGraph::new(3, edges).unwrap()); let expected = BruteForce::new().solve(&problem).unwrap().unwrap(); let actual = solve(&problem).unwrap(); assert_eq!( @@ -23,10 +23,9 @@ fn test_clique_cover_dp_minimum_intersection_graph_basis_matches_brute_force() { #[test] fn test_clique_cover_dp_minimum_intersection_graph_basis_handles_overlapping_cliques() { - let problem = MinimumIntersectionGraphBasis::new(SimpleGraph::new( - 5, - vec![(0, 1), (0, 2), (1, 2), (2, 3), (2, 4), (3, 4)], - )); + let problem = MinimumIntersectionGraphBasis::new( + SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 2), (2, 3), (2, 4), (3, 4)]).unwrap(), + ); let solution = solve(&problem).unwrap(); assert_eq!(problem.evaluate(&solution).unwrap().0, Some(2)); } @@ -37,7 +36,7 @@ fn test_intersection_basis_handles_dense_graphs_beyond_machine_word_edges() { let edges = (0..n) .flat_map(|u| ((u + 1)..n).map(move |v| (u, v))) .collect(); - let problem = MinimumIntersectionGraphBasis::new(SimpleGraph::new(n, edges)); + let problem = MinimumIntersectionGraphBasis::new(SimpleGraph::new(n, edges).unwrap()); let solution = solve(&problem).unwrap(); assert_eq!(problem.evaluate(&solution).unwrap().0, Some(1)); } diff --git a/src/unit_tests/solvers/customized/shortest_common_superstring.rs b/src/unit_tests/solvers/customized/shortest_common_superstring.rs index a331c02f7..cc1eecab8 100644 --- a/src/unit_tests/solvers/customized/shortest_common_superstring.rs +++ b/src/unit_tests/solvers/customized/shortest_common_superstring.rs @@ -1,5 +1,6 @@ use super::*; -use crate::solvers::BruteForce; +use crate::registry::load_dyn; +use crate::solvers::{BruteForce, SolveOutcome, SolverExecution, SolverRequest}; use crate::traits::Problem; #[test] @@ -11,7 +12,8 @@ fn test_subset_dp_shortest_common_superstring_matches_brute_force() { let problem = ShortestCommonSuperstring::new( 2, vec![first.clone(), second.clone(), third.clone()], - ); + ) + .unwrap(); let expected = BruteForce::new().solve(&problem).unwrap().unwrap(); let actual = solve(&problem).unwrap(); assert_eq!( @@ -34,7 +36,48 @@ fn test_subset_dp_shortest_common_superstring_handles_containment_and_scale() { vec![3, 0, 1], vec![0, 1], ], - ); - let solution = solve(&problem).unwrap(); + ) + .unwrap(); + let loaded = load_dyn( + ShortestCommonSuperstring::NAME, + &Default::default(), + serde_json::to_value(&problem).unwrap(), + ) + .unwrap(); + let result = crate::solvers::solve(&loaded, SolverRequest::Default).unwrap(); + assert!(matches!( + result.solver, + SolverExecution::Customized { + implementation: "subset-dp" + } + )); + let SolveOutcome::Optimal { + solution, + evaluation, + } = result.outcome + else { + panic!("the instance has a solution"); + }; + assert_eq!(evaluation, "Min(6)"); + let solution = serde_json::from_value(solution).unwrap(); assert_eq!(problem.evaluate(&solution).unwrap().0, Some(6)); } + +#[test] +fn subset_dp_reports_mask_and_table_size_overflow() { + for count in [usize::BITS as usize, usize::BITS as usize - 1] { + let problem = + ShortestCommonSuperstring::new(count, (0..count).map(|symbol| vec![symbol]).collect()) + .unwrap(); + let loaded = load_dyn( + ShortestCommonSuperstring::NAME, + &Default::default(), + serde_json::to_value(&problem).unwrap(), + ) + .unwrap(); + assert!(matches!( + crate::solvers::solve(&loaded, SolverRequest::Default), + Err(SolveError::IntegerOverflow(_)) + )); + } +} diff --git a/src/unit_tests/solvers/customized/solver.rs b/src/unit_tests/solvers/customized/solver.rs index 85294c3e6..a5f0f7619 100644 --- a/src/unit_tests/solvers/customized/solver.rs +++ b/src/unit_tests/solvers/customized/solver.rs @@ -1,7 +1,6 @@ use crate::models::graph::{PartialFeedbackEdgeSet, RootedTreeArrangement}; use crate::solvers::brute_force::CartesianIndices; use crate::solvers::registry::solver_capability_registry; -use crate::solvers::BruteForceProblem as _; use crate::solvers::ExactProblemKey; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; @@ -49,7 +48,7 @@ fn all_simple_graphs(num_vertices: usize) -> impl Iterator { .enumerate() .filter_map(|(bit, &edge)| ((mask & (1usize << bit)) != 0).then_some(edge)) .collect(); - SimpleGraph::new(num_vertices, edges) + SimpleGraph::new(num_vertices, edges).unwrap() }) } @@ -59,7 +58,7 @@ fn exact_partial_feedback_edge_set_feasible( max_cycle_length: usize, ) -> bool { let problem = PartialFeedbackEdgeSet::new(graph.clone(), budget, max_cycle_length); - CartesianIndices::new(problem.dimensions()) + CartesianIndices::new(crate::solvers::cartesian_dimensions(&problem).unwrap()) .unwrap() .any(|config| { let solution = crate::config::config_to_bits(&config); @@ -69,7 +68,7 @@ fn exact_partial_feedback_edge_set_feasible( fn exact_rooted_tree_arrangement_min_stretch(graph: &SimpleGraph) -> Option { let problem = RootedTreeArrangement::new(graph.clone(), i64::MAX); - CartesianIndices::new(problem.dimensions()) + CartesianIndices::new(crate::solvers::cartesian_dimensions(&problem).unwrap()) .unwrap() .filter_map(|config| problem.total_edge_stretch(&config).unwrap()) .min() @@ -77,7 +76,8 @@ fn exact_rooted_tree_arrangement_min_stretch(graph: &SimpleGraph) -> Option #[test] fn test_customized_solver_returns_none_for_unsupported_problem() { - let problem = crate::models::misc::GroupingBySwapping::new(3, vec![0, 1, 2, 0, 1, 2], 2); + let problem = + crate::models::misc::GroupingBySwapping::new(3, vec![0, 1, 2, 0, 1, 2], 2).unwrap(); let solver = CustomizedTestSolver::new(); assert!(solver.solve_dyn(&problem).is_none()); } @@ -89,7 +89,8 @@ fn test_customized_solver_matches_bruteforce_for_minimum_cardinality_key() { let problem = crate::models::set::MinimumCardinalityKey::new( 4, vec![(vec![0], vec![1]), (vec![1, 2], vec![3])], - ); + ) + .unwrap(); let brute = crate::solvers::BruteForce::new().solve(&problem).unwrap(); let custom = CustomizedTestSolver::new().solve_dyn(&problem); assert_eq!(custom.is_some(), brute.is_some()); @@ -111,7 +112,8 @@ fn test_customized_solver_matches_bruteforce_for_additional_key() { vec![(vec![0], vec![1, 2])], vec![0, 1, 2], vec![], - ); + ) + .unwrap(); let brute = crate::solvers::BruteForce::new().solve(&problem).unwrap(); let custom = CustomizedTestSolver::new().solve_dyn(&problem); assert_eq!(custom.is_some(), brute.is_some()); @@ -129,7 +131,8 @@ fn test_customized_solver_matches_bruteforce_for_prime_attribute_name() { 4, vec![(vec![0, 1], vec![2, 3]), (vec![2], vec![0])], 0, - ); + ) + .unwrap(); let brute = crate::solvers::BruteForce::new().solve(&problem).unwrap(); let custom = CustomizedTestSolver::new().solve_dyn(&problem); assert_eq!(custom.is_some(), brute.is_some()); @@ -147,7 +150,8 @@ fn test_customized_solver_matches_bruteforce_for_bcnf_violation() { 4, vec![(vec![0], vec![1]), (vec![2], vec![3])], vec![0, 1, 2, 3], - ); + ) + .unwrap(); let brute = crate::solvers::BruteForce::new().solve(&problem).unwrap(); let custom = CustomizedTestSolver::new().solve_dyn(&problem); assert_eq!(custom.is_some(), brute.is_some()); @@ -171,7 +175,8 @@ fn test_customized_solver_finds_minimum_cardinality_key_witness() { (vec![1, 3], vec![4]), (vec![2, 4], vec![5]), ], - ); + ) + .unwrap(); let witness = CustomizedTestSolver::new() .solve_dyn(&problem) .expect("expected witness"); @@ -191,7 +196,8 @@ fn test_customized_solver_finds_additional_key_witness() { ], vec![0, 1, 2, 3, 4, 5], vec![vec![0, 1], vec![2, 3], vec![4, 5]], - ); + ) + .unwrap(); let witness = CustomizedTestSolver::new() .solve_dyn(&problem) .expect("expected witness"); @@ -208,7 +214,8 @@ fn test_customized_solver_finds_prime_attribute_name_witness() { (vec![0, 3], vec![1, 2, 4, 5]), ], 3, - ); + ) + .unwrap(); let witness = CustomizedTestSolver::new() .solve_dyn(&problem) .expect("expected witness"); @@ -225,7 +232,8 @@ fn test_customized_solver_finds_bcnf_violation_witness() { (vec![3, 4], vec![5]), ], vec![0, 1, 2, 3, 4, 5], - ); + ) + .unwrap(); let witness = CustomizedTestSolver::new() .solve_dyn(&problem) .expect("expected witness"); @@ -244,14 +252,16 @@ fn test_customized_solver_no_witness_when_no_solution_exists() { ], vec![0, 1, 2], vec![vec![0], vec![1], vec![2]], - ); + ) + .unwrap(); assert!(CustomizedTestSolver::new().solve_dyn(&problem).is_none()); } #[test] fn test_customized_solver_minimum_cardinality_key_finds_minimum() { // All 3 attributes needed as a key (no single-attribute key exists) - let problem = crate::models::set::MinimumCardinalityKey::new(3, vec![(vec![0, 1], vec![2])]); + let problem = + crate::models::set::MinimumCardinalityKey::new(3, vec![(vec![0, 1], vec![2])]).unwrap(); // Both solvers should find a solution (the minimum cardinality key) let brute = crate::solvers::BruteForce::new().solve(&problem).unwrap(); let custom = CustomizedTestSolver::new().solve_dyn(&problem); @@ -278,7 +288,8 @@ fn test_customized_solver_minimum_cardinality_key_optimality() { (vec![1, 3], vec![4]), (vec![2, 4], vec![5]), ], - ); + ) + .unwrap(); let brute = crate::solvers::BruteForce::new().solve(&problem).unwrap(); let custom = CustomizedTestSolver::new().solve_dyn(&problem); assert!(brute.is_some()); @@ -309,7 +320,8 @@ fn test_customized_solver_solves_partial_feedback_edge_set_yes_and_no() { (5, 4), (0, 3), ], - ), + ) + .unwrap(), 3, 4, ); @@ -327,7 +339,8 @@ fn test_customized_solver_solves_partial_feedback_edge_set_yes_and_no() { (5, 4), (0, 3), ], - ), + ) + .unwrap(), 1, 4, ); @@ -350,7 +363,7 @@ fn test_customized_solver_solves_partial_feedback_edge_set_yes_and_no() { fn test_customized_solver_matches_bruteforce_for_partial_feedback_edge_set() { // Small instance for parity check let problem = crate::models::graph::PartialFeedbackEdgeSet::new( - crate::topology::SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 0), (2, 3)]), + crate::topology::SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 0), (2, 3)]).unwrap(), 1, 3, ); @@ -369,7 +382,7 @@ fn test_customized_solver_matches_bruteforce_for_partial_feedback_edge_set() { fn test_customized_solver_partial_feedback_edge_set_no_cycles() { // Tree graph: no cycles at all let problem = crate::models::graph::PartialFeedbackEdgeSet::new( - crate::topology::SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + crate::topology::SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), 0, 3, ); @@ -413,7 +426,7 @@ fn test_customized_solver_matches_exhaustive_search_for_small_partial_feedback_e #[test] fn test_customized_solver_finds_rooted_tree_arrangement_witness() { let problem = crate::models::graph::RootedTreeArrangement::new( - crate::topology::SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 2), (2, 3), (3, 4)]), + crate::topology::SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 2), (2, 3), (3, 4)]).unwrap(), 7, ); let witness = CustomizedTestSolver::new() @@ -429,7 +442,7 @@ fn test_customized_solver_finds_rooted_tree_arrangement_witness() { fn test_customized_solver_matches_bruteforce_for_rooted_tree_arrangement() { // Small 3-vertex instance let problem = crate::models::graph::RootedTreeArrangement::new( - crate::topology::SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + crate::topology::SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 3, ); let brute = crate::solvers::BruteForce::new().solve(&problem).unwrap(); @@ -444,7 +457,7 @@ fn test_customized_solver_matches_bruteforce_for_rooted_tree_arrangement() { fn test_customized_solver_rooted_tree_arrangement_tight_bound() { // Tight bound that rejects — path graph 0-1-2 needs at least stretch 2 let problem = crate::models::graph::RootedTreeArrangement::new( - crate::topology::SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + crate::topology::SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 1, ); // With bound=1, we need total stretch=1, but path 0-1-2 needs at minimum 2 @@ -457,7 +470,7 @@ fn test_customized_solver_rooted_tree_arrangement_tight_bound() { fn test_customized_solver_rooted_tree_arrangement_canonical_example() { // The canonical example from the model file: 4 vertices, bound=5 let problem = crate::models::graph::RootedTreeArrangement::new( - crate::topology::SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]), + crate::topology::SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]).unwrap(), 5, ); let witness = CustomizedTestSolver::new() diff --git a/src/unit_tests/solvers/decision_search.rs b/src/unit_tests/solvers/decision_search.rs index 1a56f00de..f33950f3a 100644 --- a/src/unit_tests/solvers/decision_search.rs +++ b/src/unit_tests/solvers/decision_search.rs @@ -6,24 +6,24 @@ use crate::types::{Max, Min}; #[test] fn test_decision_search_min() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumVertexCover::new(graph, vec![1i64; 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumVertexCover::new(graph, vec![1i64; 3]).unwrap(); assert_eq!(solve_via_decision(&problem, 0, 3).unwrap(), Some(1)); } #[test] fn test_decision_search_max() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MaximumIndependentSet::new(graph, vec![1i64; 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MaximumIndependentSet::new(graph, vec![1i64; 3]).unwrap(); assert_eq!(solve_via_decision(&problem, 0, 3).unwrap(), Some(2)); } #[test] fn test_decision_search_matches_brute_force() { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]); - let problem = MinimumVertexCover::new(graph, vec![1i64; 5]); + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]).unwrap(); + let problem = MinimumVertexCover::new(graph, vec![1i64; 5]).unwrap(); let solution = BruteForce::new().solve(&problem).unwrap().unwrap(); let brute_force_value = problem.evaluate(&solution).unwrap(); @@ -36,25 +36,25 @@ fn test_decision_search_matches_brute_force() { #[test] fn test_decision_search_min_returns_none_when_upper_bound_is_too_small() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumVertexCover::new(graph, vec![1i64; 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MinimumVertexCover::new(graph, vec![1i64; 3]).unwrap(); assert_eq!(solve_via_decision(&problem, 0, 0).unwrap(), None); } #[test] fn test_decision_search_max_returns_none_when_interval_is_above_optimum() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MaximumIndependentSet::new(graph, vec![1i64; 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let problem = MaximumIndependentSet::new(graph, vec![1i64; 3]).unwrap(); assert_eq!(solve_via_decision(&problem, 3, 4).unwrap(), None); } #[test] fn test_decision_search_invalid_interval_returns_none() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let min_problem = MinimumVertexCover::new(graph.clone(), vec![1i64; 3]); - let max_problem = MaximumIndependentSet::new(graph, vec![1i64; 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let min_problem = MinimumVertexCover::new(graph.clone(), vec![1i64; 3]).unwrap(); + let max_problem = MaximumIndependentSet::new(graph, vec![1i64; 3]).unwrap(); assert_eq!(solve_via_decision(&min_problem, 2, 1).unwrap(), None); assert_eq!(solve_via_decision(&max_problem, 2, 1).unwrap(), None); @@ -62,9 +62,9 @@ fn test_decision_search_invalid_interval_returns_none() { #[test] fn test_decision_search_preserves_value_direction() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let min_problem = MinimumVertexCover::new(graph.clone(), vec![1i64; 3]); - let max_problem = MaximumIndependentSet::new(graph, vec![1i64; 3]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let min_problem = MinimumVertexCover::new(graph.clone(), vec![1i64; 3]).unwrap(); + let max_problem = MaximumIndependentSet::new(graph, vec![1i64; 3]).unwrap(); let min_solution = BruteForce::new().solve(&min_problem).unwrap().unwrap(); let max_solution = BruteForce::new().solve(&max_problem).unwrap().unwrap(); diff --git a/src/unit_tests/solvers/ilp/adapter.rs b/src/unit_tests/solvers/ilp/adapter.rs new file mode 100644 index 000000000..f78545a0a --- /dev/null +++ b/src/unit_tests/solvers/ilp/adapter.rs @@ -0,0 +1,235 @@ +use super::*; +use crate::models::algebraic::{IntegerVariable, LinearConstraint}; + +#[test] +fn backend_statuses_preserve_termination_causes() { + assert_eq!(accept_backend_status(HighsModelStatus::Optimal), Ok(())); + assert_eq!( + accept_backend_status(HighsModelStatus::Infeasible), + Err(IlpBackendError::Infeasible) + ); + assert_eq!( + accept_backend_status(HighsModelStatus::Unbounded), + Err(IlpBackendError::Unbounded) + ); + assert_eq!( + accept_backend_status(HighsModelStatus::ReachedTimeLimit), + Err(IlpBackendError::Timeout) + ); + for status in [ + HighsModelStatus::UnboundedOrInfeasible, + HighsModelStatus::SolveError, + HighsModelStatus::ObjectiveBound, + HighsModelStatus::ObjectiveTarget, + HighsModelStatus::ReachedIterationLimit, + HighsModelStatus::ReachedMemoryLimit, + HighsModelStatus::ReachedSolutionLimit, + HighsModelStatus::ReachedInterrupt, + ] { + assert!(matches!(accept_backend_status(status), + Err(IlpBackendError::BackendFailure(message)) if message.contains(&format!("{status:?}")))); + } +} + +#[test] +fn native_terminals_return_the_input_ilp_solution_format() { + let adapter = HighsAdapter::new(None); + let boolean_integer = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, 1), (1, 1)], 1)], + vec![(0, 1), (1, 2)], + ObjectiveSense::Maximize, + ) + .unwrap(); + let boolean_float = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, 1.0), (1, 1.0)], 1.0)], + vec![(0, 1.0), (1, 2.0)], + ObjectiveSense::Maximize, + ) + .unwrap(); + let integer_integer = ILP::::with_variables( + vec![IntegerVariable::new(Some(-2), Some(3)).unwrap()], + vec![], + vec![(0, 1)], + ObjectiveSense::Minimize, + ) + .unwrap(); + let integer_float = ILP::::with_variables( + vec![IntegerVariable::new(Some(-2), Some(3)).unwrap()], + vec![], + vec![(0, 1.0)], + ObjectiveSense::Minimize, + ) + .unwrap(); + let values: [Vec; 4] = [ + adapter.solve(&boolean_integer).unwrap(), + adapter.solve(&boolean_float).unwrap(), + adapter.solve(&integer_integer).unwrap(), + adapter.solve(&integer_float).unwrap(), + ]; + assert_eq!(values, [vec![0, 1], vec![0, 1], vec![-2], vec![-2]]); +} + +#[test] +fn decoding_checks_shape_integrality_range_and_original_constraints() { + let ilp = ILP::::new( + 2, + vec![LinearConstraint::eq(vec![(0, 1), (1, 1)], 1)], + vec![(0, 1)], + ObjectiveSense::Maximize, + ) + .unwrap(); + assert_eq!( + decode_and_validate(&ilp, [1.00000001, 0.0]).unwrap(), + vec![1, 0] + ); + for raw in [ + vec![], + vec![1.0], + vec![1.0, 0.0, 0.0], + vec![1.0, 1.0], + vec![2.0, -1.0], + vec![0.5, 0.5], + vec![f64::NAN, 0.0], + vec![f64::INFINITY, 0.0], + vec![f64::NEG_INFINITY, 0.0], + vec![i64::MAX as f64, 0.0], + vec![i64::MIN as f64, 0.0], + ] { + assert!(matches!( + decode_and_validate(&ilp, raw), + Err(IlpBackendError::InvalidSolution(_)) + )); + } +} + +#[test] +fn validation_rejects_constraint_violations_in_both_coefficient_domains() { + let integer = ILP::::new( + 1, + vec![LinearConstraint::le(vec![(0, 2)], 1)], + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(); + assert!(matches!( + decode_and_validate(&integer, [1.0]), + Err(IlpBackendError::InvalidSolution(_)) + )); + let float = ILP::::new( + 1, + vec![LinearConstraint::le(vec![(0, 1.0)], 1.0 - 5e-10)], + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(); + assert!(!float.is_feasible(&[1]).unwrap()); + assert!(matches!( + decode_and_validate(&float, [1.0]), + Err(IlpBackendError::InvalidSolution(_)) + )); +} + +#[test] +fn validation_propagates_constraint_and_objective_overflow() { + let objective = ILP::::new( + 2, + vec![], + vec![(0, i64::MAX), (1, 1)], + ObjectiveSense::Maximize, + ) + .unwrap(); + let constraint = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, i64::MAX), (1, 1)], 0)], + vec![], + ObjectiveSense::Maximize, + ) + .unwrap(); + for ilp in [objective, constraint] { + assert!(matches!( + decode_and_validate(&ilp, [1.0, 1.0]), + Err(IlpBackendError::InvalidSolution(_)) + )); + } +} + +#[test] +fn coefficient_encoding_enforces_supported_transport_range() { + assert_eq!(BackendCoefficient::to_backend_number(17_i64).unwrap(), 17.0); + assert_eq!(BackendCoefficient::to_backend_number(0.5_f64).unwrap(), 0.5); + let value = MAX_EXACT_F64_INTEGER + 1; + for ilp in [ + ILP::::new(1, vec![], vec![(0, value)], ObjectiveSense::Maximize).unwrap(), + ILP::::new( + 1, + vec![LinearConstraint::le(vec![(0, value)], 1)], + vec![], + ObjectiveSense::Maximize, + ) + .unwrap(), + ILP::::new( + 1, + vec![LinearConstraint::le(vec![(0, 1)], value)], + vec![], + ObjectiveSense::Maximize, + ) + .unwrap(), + ] { + assert!(matches!( + HighsAdapter::new(None).solve(&ilp), + Err(IlpBackendError::InexactTransport(_)) + )); + } +} + +#[test] +fn invalid_time_limits_are_errors_instead_of_backend_panics() { + for time in [-1.0, f64::NAN, f64::INFINITY] { + assert!(matches!( + HighsAdapter::new(Some(time)).solve(&ILP::::empty()), + Err(IlpBackendError::BackendFailure(_)) + )); + } +} + +#[test] +fn adapter_accepts_an_ilp_domain_without_any_registry_entry() { + #[derive(Clone, Debug)] + struct UnregisteredDomain; + impl VariableDomain for UnregisteredDomain { + const NAME: &'static str = "UnregisteredDomain"; + fn default_variable() -> IntegerVariable { + ::default_variable() + } + fn validate_variables( + variables: &[IntegerVariable], + ) -> Result<(), crate::registry::ConstructionError> { + ::validate_variables(variables) + } + } + let ilp = ILP::::with_variables( + vec![IntegerVariable::new(Some(0), Some(2)).unwrap()], + vec![], + vec![(0, 1)], + ObjectiveSense::Maximize, + ) + .unwrap(); + assert_eq!(HighsAdapter::new(None).solve(&ilp).unwrap(), vec![2]); +} + +#[test] +fn backend_model_loading_failure_is_an_explicit_error() { + let ilp = ILP::::new( + 1, + vec![LinearConstraint::le(vec![(0, 1e30)], 1.0)], + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(); + assert!(matches!( + HighsAdapter::new(None).solve(&ilp), + Err(IlpBackendError::BackendFailure(message)) if message.contains("loading HiGHS model") + )); +} diff --git a/src/unit_tests/solvers/ilp/solver.rs b/src/unit_tests/solvers/ilp/solver.rs index 5c193553e..5c62be571 100644 --- a/src/unit_tests/solvers/ilp/solver.rs +++ b/src/unit_tests/solvers/ilp/solver.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{IntegerVariable, LinearConstraint}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::traits::Problem; fn binary_ilp( @@ -113,26 +113,6 @@ fn test_ilp_solver_rejects_inexact_integer_transport() { )); } -#[test] -fn test_backend_errors_are_classified_without_losing_the_cause() { - assert_eq!( - classify_backend_error(ResolutionError::Infeasible, None), - ILPSolveError::Infeasible, - ); - assert_eq!( - classify_backend_error(ResolutionError::Unbounded, None), - ILPSolveError::Unbounded, - ); - assert_eq!( - classify_backend_error(ResolutionError::Other("NoSolutionFound"), Some(0.1)), - ILPSolveError::Timeout, - ); - assert!(matches!( - classify_backend_error(ResolutionError::Other("SolveError"), None), - ILPSolveError::BackendFailure(message) if message.contains("SolveError") - )); -} - #[test] fn test_ilp_rejects_solution_that_is_infeasible_after_rounding() { let ilp = binary_ilp( @@ -238,7 +218,9 @@ fn test_registered_ilp_pipeline_success() { use crate::topology::SimpleGraph; use std::collections::BTreeMap; - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1_i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1_i64; 3]) + .unwrap(); let variant = BTreeMap::from([ ("graph".to_string(), "SimpleGraph".to_string()), ("weight".to_string(), "i64".to_string()), @@ -259,69 +241,43 @@ fn test_registered_ilp_pipeline_success() { } #[test] -fn test_ilp_solve_dyn_bool() { - let ilp = ILP::::new(1, vec![], vec![(0, 1.0)], ObjectiveSense::Maximize).unwrap(); - assert!(ILPSolver::new() - .solve_dyn(&ilp as &dyn std::any::Any) - .is_ok()); +fn test_float_qubo_objective_matches_reference() { + use crate::models::algebraic::QUBO; + use crate::solvers::BruteForce; + + let source = QUBO::::from_matrix(vec![ + vec![0.5, -2.5, 1.5, -4.0], + vec![0.0, -3.5, 4.0, -3.0], + vec![0.0, 0.0, 1.0, 4.5], + vec![0.0, 0.0, 0.0, -4.0], + ]) + .unwrap(); + let actual = ILPSolver::new().solve(&source).unwrap(); + let reference = BruteForce::new().solve(&source).unwrap().unwrap(); + let actual_value = source.evaluate(&actual).unwrap(); + assert!(actual_value.is_valid()); + assert_eq!(actual_value, source.evaluate(&reference).unwrap()); } #[test] -fn test_ilp_solve_dyn_i64() { - let ilp = ILP::::with_variables( - vec![ - IntegerVariable::new(Some(0), Some(3)).unwrap(), - IntegerVariable::new(Some(0), Some(3)).unwrap(), - ], - vec![], +fn test_ilp_solver_rejects_objective_overflow_after_backend_success() { + let ilp = ILP::::with_variables( + vec![IntegerVariable::new(Some(1025), Some(1025)).unwrap()], vec![], - ObjectiveSense::Minimize, + vec![(0, crate::types::MAX_EXACT_F64_INTEGER)], + ObjectiveSense::Maximize, ) .unwrap(); - assert!(ILPSolver::new() - .solve_dyn(&ilp as &dyn std::any::Any) - .is_ok()); -} - -#[test] -fn test_ilp_solve_dyn_unknown_type_returns_unsupported_problem_type() { - let result = ILPSolver::new().solve_dyn(&42_i64 as &dyn std::any::Any); - assert_eq!(result, Err(ILPSolveError::UnsupportedProblemType)); -} - -// Test acceptance policy in source-objective units, separate from variable rounding. -// This allows small absolute numerical differences near zero; it is not a -// guaranteed objective-error bound derived from HiGHS feasibility tolerances. -fn objective_close(a: f64, b: f64) -> bool { - let abs_tol = 1e-7; - let rel_tol = 1e-7; - (a - b).abs() <= abs_tol + rel_tol * a.abs().max(b.abs()) + assert!(matches!( + ILPSolver::new().solve(&ilp), + Err(ILPSolveError::InvalidSolution(_)) + )); } #[test] -fn test_float_qubo_objective_matches_reference_within_tolerance() { - use crate::models::algebraic::QUBO; - use crate::solvers::BruteForce; - - for scale in [1e-9, 1.0] { - let matrix = vec![ - vec![1.0, -5.0, 3.0, -8.0], - vec![0.0, -7.0, 8.0, -6.0], - vec![0.0, 0.0, 2.0, 9.0], - vec![0.0, 0.0, 0.0, -8.0], - ] - .into_iter() - .map(|row| row.into_iter().map(|v| v * scale).collect()) - .collect(); - let source = QUBO::::from_matrix(matrix).unwrap(); - let actual = ILPSolver::new().solve(&source).unwrap(); - let reference = BruteForce::new().solve(&source).unwrap().unwrap(); - let actual_value = source.evaluate(&actual).unwrap(); - let reference_value = source.evaluate(&reference).unwrap(); - assert!(actual_value.is_valid()); - assert!(objective_close( - actual_value.0.unwrap(), - reference_value.0.unwrap() - )); - } +fn test_invalid_public_time_limit_returns_existing_backend_error() { + assert!(matches!( + ILPSolver::with_time_limit(-1.0).solve(&ILP::::empty()), + Err(ILPSolveError::BackendFailure(_)) + )); } diff --git a/src/unit_tests/solvers/registry.rs b/src/unit_tests/solvers/registry.rs index 97c55d085..bd72b4edb 100644 --- a/src/unit_tests/solvers/registry.rs +++ b/src/unit_tests/solvers/registry.rs @@ -32,10 +32,6 @@ fn generic_decision_ilp_respects_maximization_bounds() { name: "ILP", variant: BOOL_VARIANT, }, - StaticProblemStep { - name: "ILP", - variant: FLOAT_BOOL_VARIANT, - }, ], }; let registry = build_registry( @@ -49,16 +45,17 @@ fn generic_decision_ilp_respects_maximization_bounds() { let source = ExactProblemKey::from_static(&PIPELINE.path[0]); let pipeline = registry.lookup(&source).ilp.unwrap(); let inner = MaximumIndependentSet::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); for bound in [0, 1, 2] { let decision = Decision::new(inner.clone(), bound); - let result = pipeline.solve(&decision, &crate::solvers::ILPSolver::new()); + let result = pipeline.solve(&decision, &HighsAdapter::new(None)); if bound > 1 { assert!(matches!( result, - Err(crate::solvers::ILPSolveError::UnresolvedDecision(_)) + Err(crate::solvers::ILPSolveError::Infeasible) )); assert!(BruteForce::new().solve(&decision).unwrap().is_none()); continue; @@ -72,22 +69,22 @@ fn generic_decision_ilp_respects_maximization_bounds() { } #[test] -fn generic_decision_ilp_reports_unresolved_but_preserves_extraction_errors() { +fn generic_decision_ilp_reports_no_but_preserves_extraction_errors() { use crate::models::decision::Decision; use crate::models::graph::MinimumVertexCover; use crate::rules::{ExtractionError, ReductionResult}; - use crate::solvers::{ILPSolveError, ILPSolver}; + use crate::solvers::ILPSolveError; use crate::topology::SimpleGraph; use crate::traits::Problem; type Inner = MinimumVertexCover; - struct BrokenExtractor(Inner); + struct BrokenExtractor(Decision); impl ReductionResult for BrokenExtractor { type Source = Decision; type Target = Inner; fn target_problem(&self) -> &Inner { - &self.0 + self.0.inner() } fn extract_solution(&self, _: &Vec) -> crate::rules::ExtractionResult> { @@ -95,6 +92,20 @@ fn generic_decision_ilp_reports_unresolved_but_preserves_extraction_errors() { } } + impl crate::rules::AggregateReductionResult for BrokenExtractor { + type Source = Decision; + type Target = Inner; + fn target_problem(&self) -> &Inner { + self.0.inner() + } + fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { + crate::types::Or(crate::types::OptimizationValue::meets_bound( + &value, + self.0.bound(), + )) + } + } + let source = ExactProblemKey::new( Decision::::NAME, Decision::::variant() @@ -108,17 +119,33 @@ fn generic_decision_ilp_reports_unresolved_but_preserves_extraction_errors() { path: original.path.clone(), reducers: original.reducers.clone(), }; - pipeline.reducers[0].0 = |source| { + pipeline.reducers[0] = |source| { let source = source.downcast_ref::>().unwrap(); - Ok(Box::new(BrokenExtractor(source.inner().clone()))) + let result = std::rc::Rc::new(BrokenExtractor(source.clone())); + Ok(crate::rules::registry::ExecutedStep { + aggregate: Some(result.clone()), + interpret_optimum: Some({ + let result = result.clone(); + std::rc::Rc::new(move |solution: &dyn std::any::Any| { + let solution = solution.downcast_ref::>().unwrap(); + let value = result.0.inner().evaluate(solution)?; + Ok(crate::rules::AggregateReductionResult::extract_value( + result.as_ref(), + value, + ) + .is_valid()) + }) + }), + witness: result, + }) }; - let inner = Inner::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i64; 2]); + let inner = Inner::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![1i64; 2]).unwrap(); assert!(matches!( - pipeline.solve(&Decision::new(inner.clone(), 0), &ILPSolver::new()), - Err(ILPSolveError::UnresolvedDecision(_)) + pipeline.solve(&Decision::new(inner.clone(), 0), &HighsAdapter::new(None)), + Err(ILPSolveError::Infeasible) )); assert!(matches!( - pipeline.solve(&Decision::new(inner, 1), &ILPSolver::new()), + pipeline.solve(&Decision::new(inner, 1), &HighsAdapter::new(None)), Err(ILPSolveError::Extraction(ExtractionError::Reduction { message, .. })) if message == "broken witness decoder" )); @@ -406,11 +433,7 @@ fn solver_capability_registry_exposes_representative_capability_classes() { assert!(direct_ilp.customized.is_none()); assert_eq!( direct_ilp.ilp.unwrap().path_labels(), - [ - "MaximumClique", - "ILP", - "ILP" - ] + ["MaximumClique", "ILP"] ); let multihop_ilp = solver_capabilities(&key( @@ -435,10 +458,7 @@ fn solver_capability_registry_exposes_representative_capability_classes() { let ilp_itself = solver_capabilities(&key("ILP", &[("variable", "bool"), ("coefficient", "i64")])).unwrap(); - assert_eq!( - ilp_itself.ilp.unwrap().path_labels(), - ["ILP", "ILP"] - ); + assert_eq!(ilp_itself.ilp.unwrap().path_labels(), ["ILP"]); } #[test] @@ -538,18 +558,12 @@ fn solver_capability_registry_ignores_unrelated_reduction_edges() { minimal_pipeline .reducers .iter() - .map(|(reducer, aggregate)| ( - *reducer as usize, - aggregate.map(|reduce| reduce as usize) - )) + .map(|reducer| *reducer as usize) .collect::>(), expanded_pipeline .reducers .iter() - .map(|(reducer, aggregate)| ( - *reducer as usize, - aggregate.map(|reduce| reduce as usize) - )) + .map(|reducer| *reducer as usize) .collect::>() ); } @@ -588,3 +602,34 @@ fn solver_capability_registry_ambiguous_exact_edge_is_rejected() { RegistryBuildError::InvalidEdge { matches: 2, .. } )); } + +#[test] +fn native_terminal_dispatch_rejects_non_ilp_values() { + assert_eq!( + solve_ilp_terminal(&42_i64, &HighsAdapter::new(None)), + Err(crate::solvers::ILPSolveError::UnsupportedProblemType) + ); +} + +#[test] +fn registered_pipelines_stop_at_the_first_native_ilp() { + let registry = solver_capability_registry().unwrap(); + for pipeline in registry.ilp.values() { + assert!(pipeline.path.last().unwrap().is_supported_ilp()); + assert!(pipeline.path[..pipeline.path.len() - 1] + .iter() + .all(|step| !step.is_supported_ilp())); + } + for variable in ["bool", "i64"] { + for coefficient in ["i64", "f64"] { + let key = ExactProblemKey::new( + "ILP", + BTreeMap::from([ + ("variable".into(), variable.into()), + ("coefficient".into(), coefficient.into()), + ]), + ); + assert_eq!(registry.lookup(&key).ilp.unwrap().path(), &[key]); + } + } +} diff --git a/src/unit_tests/solvers/resolver.rs b/src/unit_tests/solvers/resolver.rs index 8cc05f909..80065cd1c 100644 --- a/src/unit_tests/solvers/resolver.rs +++ b/src/unit_tests/solvers/resolver.rs @@ -42,16 +42,6 @@ fn decision_reductions_check_target_optimum_before_extracting_witness() { SolverRequest::Default, ] { let result = solve(&problem, backend); - if matches!( - &result, - Err(crate::solvers::SolveError::IlpSolve { - source: crate::solvers::ILPSolveError::UnresolvedDecision(_), - .. - }) - ) { - assert!(!expected, "{name}, {backend:?}"); - continue; - } match result.unwrap().outcome { SolveOutcome::Optimal { solution, @@ -59,7 +49,10 @@ fn decision_reductions_check_target_optimum_before_extracting_witness() { } => { assert!(expected, "{name}, {backend:?}"); assert_eq!(evaluation, "Or(true)"); - assert_eq!(problem.evaluate_dyn(&solution).unwrap(), "Or(true)"); + assert_eq!( + problem.evaluate_dyn(&solution).unwrap(), + ("Or(true)".into(), true) + ); } SolveOutcome::Infeasible => assert!(!expected, "{name}, {backend:?}"), } @@ -85,16 +78,6 @@ fn hamiltonian_ilp_matches_exhaustive_search_on_small_graphs() { .unwrap(); let reference = solve(&problem, SolverRequest::BruteForce).unwrap(); let actual = solve(&problem, SolverRequest::Ilp); - if matches!( - &actual, - Err(crate::solvers::SolveError::IlpSolve { - source: crate::solvers::ILPSolveError::UnresolvedDecision(_), - .. - }) - ) { - assert!(matches!(reference.outcome, SolveOutcome::Infeasible)); - continue; - } let actual = actual.unwrap(); assert_eq!( matches!(actual.outcome, SolveOutcome::Infeasible), @@ -102,7 +85,10 @@ fn hamiltonian_ilp_matches_exhaustive_search_on_small_graphs() { "graph {mask}" ); if let SolveOutcome::Optimal { solution, .. } = actual.outcome { - assert_eq!(problem.evaluate_dyn(&solution).unwrap(), "Or(true)"); + assert_eq!( + problem.evaluate_dyn(&solution).unwrap(), + ("Or(true)".into(), true) + ); } } } @@ -114,7 +100,7 @@ fn generic_decision_ilp_compares_inner_optimum_with_bound() { }; use crate::topology::SimpleGraph; - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(); let weighted_variant = BTreeMap::from([ ("graph".to_string(), "SimpleGraph".to_string()), ("weight".to_string(), "i64".to_string()), @@ -123,13 +109,15 @@ fn generic_decision_ilp_compares_inner_optimum_with_bound() { ( "DecisionMinimumVertexCover", weighted_variant.clone(), - serde_json::to_value(MinimumVertexCover::new(graph.clone(), vec![1i64; 3])).unwrap(), + serde_json::to_value(MinimumVertexCover::new(graph.clone(), vec![1i64; 3]).unwrap()) + .unwrap(), 2, ), ( "DecisionMinimumDominatingSet", weighted_variant, - serde_json::to_value(MinimumDominatingSet::new(graph.clone(), vec![1i64; 3])).unwrap(), + serde_json::to_value(MinimumDominatingSet::new(graph.clone(), vec![1i64; 3]).unwrap()) + .unwrap(), 1, ), ( @@ -153,19 +141,6 @@ fn generic_decision_ilp_compares_inner_optimum_with_bound() { SolverRequest::Default, ] { let result = solve(&loaded, backend); - if bound < optimum && backend != SolverRequest::BruteForce { - assert!( - matches!( - result, - Err(crate::solvers::SolveError::IlpSolve { - source: crate::solvers::ILPSolveError::UnresolvedDecision(_), - .. - }) - ), - "{name}, {bound}, {backend:?}" - ); - continue; - } let result = result.unwrap(); if bound < optimum { assert_eq!( @@ -182,7 +157,10 @@ fn generic_decision_ilp_compares_inner_optimum_with_bound() { panic!("expected a witness for {name}, {bound}, {backend:?}"); }; assert_eq!(evaluation, "Or(true)"); - assert_eq!(loaded.evaluate_dyn(&solution).unwrap(), "Or(true)"); + assert_eq!( + loaded.evaluate_dyn(&solution).unwrap(), + ("Or(true)".into(), true) + ); } } } @@ -209,19 +187,24 @@ fn generic_decision_ilp_matches_exhaustive_search_on_small_graphs() { .enumerate() .filter_map(|(i, edge)| (mask & (1 << i) != 0).then_some(edge)) .collect(), - ); + ) + .unwrap(); let models = [ ( "DecisionMinimumVertexCover", &weighted, - serde_json::to_value(MinimumVertexCover::new(graph.clone(), vec![1i64, 2, 3])) - .unwrap(), + serde_json::to_value( + MinimumVertexCover::new(graph.clone(), vec![1i64, 2, 3]).unwrap(), + ) + .unwrap(), ), ( "DecisionMinimumDominatingSet", &weighted, - serde_json::to_value(MinimumDominatingSet::new(graph.clone(), vec![1i64, 2, 3])) - .unwrap(), + serde_json::to_value( + MinimumDominatingSet::new(graph.clone(), vec![1i64, 2, 3]).unwrap(), + ) + .unwrap(), ), ( "DecisionOptimalLinearArrangement", @@ -239,19 +222,6 @@ fn generic_decision_ilp_matches_exhaustive_search_on_small_graphs() { .unwrap(); let reference = solve(&loaded, SolverRequest::BruteForce).unwrap(); let actual = solve(&loaded, SolverRequest::Ilp); - if matches!(reference.outcome, SolveOutcome::Infeasible) { - assert!( - matches!( - actual, - Err(crate::solvers::SolveError::IlpSolve { - source: crate::solvers::ILPSolveError::UnresolvedDecision(_), - .. - }) - ), - "{name}, graph {mask}, bound {bound}" - ); - continue; - } let actual = actual.unwrap(); assert_eq!( matches!(actual.outcome, SolveOutcome::Infeasible), @@ -259,7 +229,10 @@ fn generic_decision_ilp_matches_exhaustive_search_on_small_graphs() { "{name}, graph {mask}, bound {bound}" ); if let SolveOutcome::Optimal { solution, .. } = actual.outcome { - assert_eq!(loaded.evaluate_dyn(&solution).unwrap(), "Or(true)"); + assert_eq!( + loaded.evaluate_dyn(&solution).unwrap(), + ("Or(true)".into(), true) + ); } } } @@ -270,7 +243,7 @@ fn generic_decision_ilp_matches_exhaustive_search_on_small_graphs() { fn deterministic_solver_dispatch_customized_registration_wins_default_dispatch() { use crate::models::set::MinimumCardinalityKey; - let problem = MinimumCardinalityKey::new(3, vec![(vec![0], vec![1, 2])]); + let problem = MinimumCardinalityKey::new(3, vec![(vec![0], vec![1, 2])]).unwrap(); let loaded = crate::registry::load_dyn( MinimumCardinalityKey::NAME, &BTreeMap::new(), @@ -295,7 +268,7 @@ fn deterministic_solver_dispatch_unregistered_customized_override_is_a_capabilit use crate::models::graph::MaxCut; use crate::topology::SimpleGraph; - let problem = MaxCut::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i64]); + let problem = MaxCut::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![1i64]).unwrap(); let loaded = crate::registry::load_dyn( MaxCut::::NAME, &BTreeMap::from([ @@ -322,7 +295,7 @@ fn deterministic_solver_dispatch_unregistered_ilp_override_is_a_capability_error // MaxCut has a discoverable graph route toward ILP, but that route is // partial for valid negative-weight instances and is intentionally not a // registered solver pipeline. - let problem = MaxCut::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i64]); + let problem = MaxCut::new(SimpleGraph::new(2, vec![(0, 1)]).unwrap(), vec![1i64]).unwrap(); let loaded = crate::registry::load_dyn( MaxCut::::NAME, &BTreeMap::from([ @@ -349,7 +322,8 @@ fn deterministic_solver_dispatch_customized_infeasibility_does_not_fall_back() { // {0} is the only candidate key and it is already known, so the registered // customized solver has no witness. Brute force can still report the aggregate // infeasibility result, which lets this test distinguish fallback from error. - let problem = AdditionalKey::new(3, vec![(vec![0], vec![1, 2])], vec![0, 1, 2], vec![vec![0]]); + let problem = + AdditionalKey::new(3, vec![(vec![0], vec![1, 2])], vec![0, 1, 2], vec![vec![0]]).unwrap(); let loaded = load_dyn( AdditionalKey::NAME, &BTreeMap::new(), @@ -365,7 +339,7 @@ fn deterministic_solver_dispatch_customized_infeasibility_does_not_fall_back() { } #[test] -fn deterministic_solver_dispatch_integer_ilp_uses_registered_cast_pipeline() { +fn deterministic_solver_dispatch_integer_ilp_uses_native_terminal() { let problem = ILP::::new(0, vec![], vec![], ObjectiveSense::Minimize).unwrap(); let loaded = load_dyn( ILP::::NAME, @@ -381,7 +355,7 @@ fn deterministic_solver_dispatch_integer_ilp_uses_registered_cast_pipeline() { assert_eq!( result.solver, SolverExecution::Ilp { - reduction_path: vec!["ILP".to_string(), "ILP".to_string()] + reduction_path: vec!["ILP".to_string()] } ); assert!(matches!( @@ -471,9 +445,10 @@ fn deterministic_solver_dispatch_fixed_multihop_pipeline_is_repeatable() { use crate::topology::SimpleGraph; let problem = MaximumIndependentSet::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![crate::types::One; 3], - ); + ) + .unwrap(); let variant = BTreeMap::from([ ("graph".to_string(), "SimpleGraph".to_string()), ("weight".to_string(), "One".to_string()), @@ -498,7 +473,6 @@ fn deterministic_solver_dispatch_fixed_multihop_pipeline_is_repeatable() { "MaximumIndependentSet", "MaximumSetPacking", "ILP", - "ILP", ] ); } @@ -508,7 +482,7 @@ fn deterministic_solver_dispatch_customized_default_allows_explicit_ilp_override use crate::models::graph::RootedTreeArrangement; use crate::topology::SimpleGraph; - let problem = RootedTreeArrangement::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 3); + let problem = RootedTreeArrangement::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 3); let loaded = load_dyn( RootedTreeArrangement::::NAME, &BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]), @@ -543,7 +517,7 @@ fn deterministic_solver_dispatch_repeats_each_available_solver_class() { use crate::models::graph::RootedTreeArrangement; use crate::topology::SimpleGraph; - let problem = RootedTreeArrangement::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 3); + let problem = RootedTreeArrangement::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 3); let loaded = load_dyn( RootedTreeArrangement::::NAME, &BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]), @@ -589,22 +563,6 @@ fn check_unit_dominating_decision(num_vertices: usize, edges: &[(usize, usize)], let reference = solve(&problem, SolverRequest::BruteForce).unwrap(); for backend in [SolverRequest::Ilp, SolverRequest::Default] { let actual = solve(&problem, backend); - if matches!(reference.outcome, SolveOutcome::Infeasible) { - assert!( - matches!( - actual, - Err(crate::solvers::SolveError::IlpSolve { - source: crate::solvers::ILPSolveError::UnresolvedDecision(_), - .. - }) | Ok(crate::solvers::SolveResult { - outcome: SolveOutcome::Infeasible, - .. - }) - ), - "n={num_vertices}, edges={edges:?}, bound={bound}" - ); - continue; - } let actual = actual.unwrap(); let SolverExecution::Ilp { reduction_path } = &actual.solver else { panic!("expected the registered ILP pipeline"); @@ -626,7 +584,10 @@ fn check_unit_dominating_decision(num_vertices: usize, edges: &[(usize, usize)], } = actual.outcome { assert_eq!(evaluation, "Or(true)"); - assert_eq!(problem.evaluate_dyn(&solution).unwrap(), "Or(true)"); + assert_eq!( + problem.evaluate_dyn(&solution).unwrap(), + ("Or(true)".into(), true) + ); } } } diff --git a/src/unit_tests/symbolic_parameter_contracts.rs b/src/unit_tests/symbolic_parameter_contracts.rs index 1ba9b0d78..b3edbf17f 100644 --- a/src/unit_tests/symbolic_parameter_contracts.rs +++ b/src/unit_tests/symbolic_parameter_contracts.rs @@ -10,9 +10,10 @@ use crate::Problem; #[test] fn exact_rule_formula_matches_the_constructed_target() { let source = MaximumIndependentSet::::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(), vec![1; 5], - ); + ) + .unwrap(); let reduction = as ReduceTo< MaximumClique, >>::reduce_to(&source) @@ -56,7 +57,7 @@ fn exact_rule_formula_matches_the_constructed_target() { #[test] fn incoming_rule_measures_every_declared_field_on_a_sink_variant() { - let source = ExactCoverBy3Sets::new(3, vec![[0, 1, 2]]); + let source = ExactCoverBy3Sets::new(3, vec![[0, 1, 2]]).unwrap(); let reduction = >::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); diff --git a/src/unit_tests/topology/bipartite_graph.rs b/src/unit_tests/topology/bipartite_graph.rs index 46e3b381a..0c5294fff 100644 --- a/src/unit_tests/topology/bipartite_graph.rs +++ b/src/unit_tests/topology/bipartite_graph.rs @@ -4,7 +4,7 @@ use crate::topology::{BipartiteGraph, Graph}; fn test_bipartite_graph_basic() { // K_{2,3}: left={0,1}, right={0,1,2}, all edges let edges = vec![(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)]; - let g = BipartiteGraph::new(2, 3, edges); + let g = BipartiteGraph::new(2, 3, edges).unwrap(); assert_eq!(g.num_vertices(), 5); assert_eq!(g.num_edges(), 6); assert_eq!(g.left_size(), 2); @@ -13,7 +13,7 @@ fn test_bipartite_graph_basic() { #[test] fn test_bipartite_graph_edges_unified() { - let g = BipartiteGraph::new(1, 2, vec![(0, 0), (0, 1)]); + let g = BipartiteGraph::new(1, 2, vec![(0, 0), (0, 1)]).unwrap(); let edges = g.edges(); assert!(edges.contains(&(0, 1))); assert!(edges.contains(&(0, 2))); @@ -22,7 +22,7 @@ fn test_bipartite_graph_edges_unified() { #[test] fn test_bipartite_graph_has_edge() { - let g = BipartiteGraph::new(2, 2, vec![(0, 0), (1, 1)]); + let g = BipartiteGraph::new(2, 2, vec![(0, 0), (1, 1)]).unwrap(); assert!(g.has_edge(0, 2)); assert!(g.has_edge(1, 3)); assert!(!g.has_edge(0, 1)); @@ -31,7 +31,7 @@ fn test_bipartite_graph_has_edge() { #[test] fn test_bipartite_graph_neighbors() { - let g = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 1)]); + let g = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 1)]).unwrap(); let mut n0 = g.neighbors(0); n0.sort(); assert_eq!(n0, vec![2, 3]); @@ -43,18 +43,32 @@ fn test_bipartite_graph_neighbors() { #[test] fn test_bipartite_graph_left_edges() { let edges = vec![(0, 0), (1, 1)]; - let g = BipartiteGraph::new(2, 2, edges.clone()); + let g = BipartiteGraph::new(2, 2, edges.clone()).unwrap(); assert_eq!(g.left_edges(), &edges); } #[test] -#[should_panic] fn test_bipartite_graph_invalid_left_index() { - BipartiteGraph::new(2, 2, vec![(2, 0)]); + assert!(BipartiteGraph::new(2, 2, vec![(2, 0)]).is_err()); } #[test] -#[should_panic] fn test_bipartite_graph_invalid_right_index() { - BipartiteGraph::new(2, 2, vec![(0, 2)]); + assert!(BipartiteGraph::new(2, 2, vec![(0, 2)]).is_err()); +} + +#[test] +fn deserialize_checks_partition_endpoints_and_total_size() { + for edges in [vec![(1, 0)], vec![(0, 1)]] { + assert!(serde_json::from_value::(serde_json::json!({ + "left_size": 1, "right_size": 1, "edges": edges + })) + .is_err()); + } + assert!(BipartiteGraph::new(usize::MAX, 1, vec![]).is_err()); + let graph: BipartiteGraph = serde_json::from_value(serde_json::json!({ + "left_size": 1, "right_size": 1, "edges": [[0, 0]] + })) + .unwrap(); + assert_eq!(graph.edges(), vec![(0, 1)]); } diff --git a/src/unit_tests/topology/directed_graph.rs b/src/unit_tests/topology/directed_graph.rs index 086728ae0..ab86ab419 100644 --- a/src/unit_tests/topology/directed_graph.rs +++ b/src/unit_tests/topology/directed_graph.rs @@ -2,7 +2,7 @@ use super::*; #[test] fn test_directed_graph_new() { - let g = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let g = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); assert_eq!(g.num_vertices(), 4); assert_eq!(g.num_arcs(), 3); } @@ -14,13 +14,13 @@ fn test_directed_graph_empty() { assert_eq!(g.num_arcs(), 0); assert!(!g.is_empty()); - let empty = DirectedGraph::new(0, vec![]); + let empty = DirectedGraph::new(0, vec![]).unwrap(); assert!(empty.is_empty()); } #[test] fn test_directed_graph_arcs() { - let g = DirectedGraph::new(3, vec![(0, 1), (2, 0)]); + let g = DirectedGraph::new(3, vec![(0, 1), (2, 0)]).unwrap(); let mut arcs = g.arcs(); arcs.sort(); assert_eq!(arcs, vec![(0, 1), (2, 0)]); @@ -28,7 +28,7 @@ fn test_directed_graph_arcs() { #[test] fn test_directed_graph_has_arc() { - let g = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + let g = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); assert!(g.has_arc(0, 1)); assert!(g.has_arc(1, 2)); assert!(!g.has_arc(1, 0)); // Directed: reverse not present @@ -38,7 +38,7 @@ fn test_directed_graph_has_arc() { #[test] fn test_directed_graph_successors() { // 0 → 1, 0 → 2, 1 → 2 - let g = DirectedGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); + let g = DirectedGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); let mut succ0 = g.successors(0); succ0.sort(); assert_eq!(succ0, vec![1, 2]); @@ -51,7 +51,7 @@ fn test_directed_graph_successors() { #[test] fn test_directed_graph_predecessors() { // 0 → 1, 0 → 2, 1 → 2 - let g = DirectedGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); + let g = DirectedGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); assert_eq!(g.predecessors(0), Vec::::new()); let mut pred2 = g.predecessors(2); pred2.sort(); @@ -61,7 +61,7 @@ fn test_directed_graph_predecessors() { #[test] fn test_directed_graph_degrees() { - let graph = DirectedGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); + let graph = DirectedGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]).unwrap(); assert_eq!(graph.out_degree(0), 2); assert_eq!(graph.out_degree(1), 1); assert_eq!(graph.out_degree(2), 0); @@ -73,14 +73,14 @@ fn test_directed_graph_degrees() { #[test] fn test_directed_graph_is_dag_true() { // Simple path: 0 → 1 → 2 - let g = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + let g = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); assert!(g.is_dag()); } #[test] fn test_directed_graph_is_dag_false() { // Cycle: 0 → 1 → 2 → 0 - let g = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); + let g = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); assert!(!g.is_dag()); } @@ -93,25 +93,25 @@ fn test_directed_graph_is_dag_empty() { #[test] fn test_directed_graph_is_dag_self_loop() { // Self-loop is a cycle - let g = DirectedGraph::new(2, vec![(0, 0)]); + let g = DirectedGraph::new(2, vec![(0, 0)]).unwrap(); assert!(!g.is_dag()); } #[test] fn test_is_strongly_connected_cycle() { - let g = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); + let g = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); assert!(g.is_strongly_connected()); } #[test] fn test_is_strongly_connected_path() { - let g = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + let g = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); assert!(!g.is_strongly_connected()); } #[test] fn test_is_strongly_connected_single_vertex() { - let g = DirectedGraph::new(1, vec![]); + let g = DirectedGraph::new(1, vec![]).unwrap(); assert!(g.is_strongly_connected()); } @@ -124,7 +124,7 @@ fn test_is_strongly_connected_empty() { #[test] fn test_directed_graph_is_acyclic_subgraph() { // Cycle: 0->1->2->0 - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); // Keep all arcs -> has cycle assert!(!graph.is_acyclic_subgraph(&[true, true, true])); // Remove arc 2->0 -> acyclic @@ -138,7 +138,7 @@ fn test_directed_graph_is_acyclic_subgraph() { #[test] fn test_directed_graph_induced_subgraph_basic() { // 0 → 1 → 2 → 0 (cycle), keep vertices 0 and 1 (drop 2) - let g = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); + let g = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); let subg = g.induced_subgraph(&[true, true, false]); // After dropping vertex 2: vertices 0 and 1 remain, arc (0→1) remains // Vertex remapping: 0→0, 1→1 @@ -153,7 +153,7 @@ fn test_directed_graph_induced_subgraph_basic() { fn test_directed_graph_induced_subgraph_remapping() { // Vertices 0, 1, 2, 3; keep 1 and 3 only // Arcs: 1 → 3 - let g = DirectedGraph::new(4, vec![(0, 1), (1, 3), (2, 0)]); + let g = DirectedGraph::new(4, vec![(0, 1), (1, 3), (2, 0)]).unwrap(); let subg = g.induced_subgraph(&[false, true, false, true]); // Vertex 1 → new index 0, vertex 3 → new index 1 assert_eq!(subg.num_vertices(), 2); @@ -164,7 +164,7 @@ fn test_directed_graph_induced_subgraph_remapping() { #[test] fn test_directed_graph_induced_subgraph_no_cross_arcs() { // Keep a subset that has no arcs between kept vertices - let g = DirectedGraph::new(3, vec![(0, 2), (1, 2)]); + let g = DirectedGraph::new(3, vec![(0, 2), (1, 2)]).unwrap(); // Keep 0 and 1 only — neither arc (0→2) nor (1→2) is kept (2 dropped) let subg = g.induced_subgraph(&[true, true, false]); assert_eq!(subg.num_vertices(), 2); @@ -173,36 +173,36 @@ fn test_directed_graph_induced_subgraph_no_cross_arcs() { #[test] fn test_directed_graph_eq_same_order() { - let g1 = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); - let g2 = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + let g1 = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let g2 = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); assert_eq!(g1, g2); } #[test] fn test_directed_graph_eq_different_arc_order() { // Same arcs, provided in different order - let g1 = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let g2 = DirectedGraph::new(3, vec![(2, 0), (0, 1), (1, 2)]); + let g1 = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(); + let g2 = DirectedGraph::new(3, vec![(2, 0), (0, 1), (1, 2)]).unwrap(); assert_eq!(g1, g2); } #[test] fn test_directed_graph_ne_different_arcs() { - let g1 = DirectedGraph::new(3, vec![(0, 1)]); - let g2 = DirectedGraph::new(3, vec![(1, 0)]); // Reversed direction + let g1 = DirectedGraph::new(3, vec![(0, 1)]).unwrap(); + let g2 = DirectedGraph::new(3, vec![(1, 0)]).unwrap(); // Reversed direction assert_ne!(g1, g2); } #[test] fn test_directed_graph_ne_different_vertices() { - let g1 = DirectedGraph::new(3, vec![(0, 1)]); - let g2 = DirectedGraph::new(4, vec![(0, 1)]); + let g1 = DirectedGraph::new(3, vec![(0, 1)]).unwrap(); + let g2 = DirectedGraph::new(4, vec![(0, 1)]).unwrap(); assert_ne!(g1, g2); } #[test] fn test_directed_graph_serialization() { - let g = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]); + let g = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]).unwrap(); let json = serde_json::to_string(&g).expect("serialization failed"); let restored: DirectedGraph = serde_json::from_str(&json).expect("deserialization failed"); assert_eq!(g, restored); @@ -210,7 +210,7 @@ fn test_directed_graph_serialization() { #[test] fn test_directed_graph_json_roundtrip() { - let g = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let g = DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let json = serde_json::to_value(&g).unwrap(); assert_eq!(json["num_vertices"], 4); let arcs: Vec<(usize, usize)> = serde_json::from_value(json["arcs"].clone()).unwrap(); @@ -221,7 +221,7 @@ fn test_directed_graph_json_roundtrip() { #[test] fn test_directed_graph_json_format() { - let g = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + let g = DirectedGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let json_str = serde_json::to_string(&g).unwrap(); assert!(!json_str.contains("edge_property")); assert!(!json_str.contains("node_holes")); @@ -230,7 +230,14 @@ fn test_directed_graph_json_format() { } #[test] -#[should_panic(expected = "arc (0, 5) references vertex >= num_vertices")] fn test_directed_graph_invalid_arc() { - DirectedGraph::new(3, vec![(0, 5)]); + assert!(DirectedGraph::new(3, vec![(0, 5)]).is_err()); +} + +#[test] +fn deserialize_rejects_out_of_range_arcs() { + assert!(serde_json::from_value::(serde_json::json!({ + "num_vertices": 2, "arcs": [[2, 0]] + })) + .is_err()); } diff --git a/src/unit_tests/topology/graph.rs b/src/unit_tests/topology/graph.rs index da89fb5ba..07d946ec3 100644 --- a/src/unit_tests/topology/graph.rs +++ b/src/unit_tests/topology/graph.rs @@ -2,7 +2,7 @@ use super::*; #[test] fn test_simple_graph_new() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); assert_eq!(graph.num_vertices(), 4); assert_eq!(graph.num_edges(), 3); } @@ -52,7 +52,7 @@ fn test_simple_graph_star() { #[test] fn test_simple_graph_grid() { - let graph = SimpleGraph::grid(2, 3); + let graph = SimpleGraph::grid(2, 3).unwrap(); assert_eq!(graph.num_vertices(), 6); // 2 rows: 2 horizontal edges per row = 4 // 3 cols: 1 vertical edge per col = 3 @@ -61,7 +61,7 @@ fn test_simple_graph_grid() { #[test] fn test_simple_graph_has_edge() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); assert!(graph.has_edge(0, 1)); assert!(graph.has_edge(1, 0)); // Undirected assert!(graph.has_edge(1, 2)); @@ -70,7 +70,7 @@ fn test_simple_graph_has_edge() { #[test] fn test_simple_graph_neighbors() { - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(); let mut neighbors = graph.neighbors(0); neighbors.sort(); assert_eq!(neighbors, vec![1, 2, 3]); @@ -79,7 +79,7 @@ fn test_simple_graph_neighbors() { #[test] fn test_simple_graph_degree() { - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)]).unwrap(); assert_eq!(graph.degree(0), 3); assert_eq!(graph.degree(1), 1); } @@ -95,7 +95,7 @@ fn test_simple_graph_is_empty() { #[test] fn test_simple_graph_for_each_edge() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let mut count = 0; graph.for_each_edge(|_, _| count += 1); assert_eq!(count, 2); @@ -103,18 +103,17 @@ fn test_simple_graph_for_each_edge() { #[test] fn test_simple_graph_eq() { - let g1 = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let g2 = SimpleGraph::new(3, vec![(1, 2), (0, 1)]); // Different order - let g3 = SimpleGraph::new(3, vec![(0, 1)]); + let g1 = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); + let g2 = SimpleGraph::new(3, vec![(1, 2), (0, 1)]).unwrap(); // Different order + let g3 = SimpleGraph::new(3, vec![(0, 1)]).unwrap(); assert_eq!(g1, g2); assert_ne!(g1, g3); } #[test] -#[should_panic(expected = "edge (0, 5) references vertex >= num_vertices")] fn test_simple_graph_invalid_edge() { - SimpleGraph::new(3, vec![(0, 5)]); + assert!(SimpleGraph::new(3, vec![(0, 5)]).is_err()); } #[test] @@ -129,14 +128,14 @@ fn test_simple_graph_cycle_small() { #[test] fn test_simple_graph_eq_different_sizes() { // Test PartialEq when graphs have different sizes - let g1 = SimpleGraph::new(3, vec![(0, 1)]); - let g2 = SimpleGraph::new(4, vec![(0, 1)]); // Different vertex count + let g1 = SimpleGraph::new(3, vec![(0, 1)]).unwrap(); + let g2 = SimpleGraph::new(4, vec![(0, 1)]).unwrap(); // Different vertex count assert_ne!(g1, g2); } #[test] fn test_simplegraph_json_roundtrip() { - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); let json = serde_json::to_value(&graph).unwrap(); assert_eq!(json["num_vertices"], 4); let edges: Vec<(usize, usize)> = serde_json::from_value(json["edges"].clone()).unwrap(); @@ -147,9 +146,18 @@ fn test_simplegraph_json_roundtrip() { #[test] fn test_simplegraph_json_format() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); let json_str = serde_json::to_string(&graph).unwrap(); assert!(!json_str.contains("edge_property")); assert!(!json_str.contains("node_holes")); assert!(json_str.contains("num_vertices")); } + +#[test] +fn deserialize_rejects_out_of_range_edges() { + assert!(serde_json::from_value::(serde_json::json!({ + "num_vertices": 2, "edges": [[0, 2]] + })) + .is_err()); + assert!(SimpleGraph::grid(usize::MAX, 2).is_err()); +} diff --git a/src/unit_tests/topology/mixed_graph.rs b/src/unit_tests/topology/mixed_graph.rs index 316e4ac02..d9074763e 100644 --- a/src/unit_tests/topology/mixed_graph.rs +++ b/src/unit_tests/topology/mixed_graph.rs @@ -2,7 +2,7 @@ use crate::topology::MixedGraph; #[test] fn test_mixed_graph_creation_and_counts() { - let graph = MixedGraph::new(4, vec![(0, 1), (2, 3)], vec![(0, 2), (1, 3)]); + let graph = MixedGraph::new(4, vec![(0, 1), (2, 3)], vec![(0, 2), (1, 3)]).unwrap(); assert_eq!(graph.num_vertices(), 4); assert_eq!(graph.num_arcs(), 2); @@ -19,7 +19,7 @@ fn test_mixed_graph_creation_and_counts() { #[test] fn test_mixed_graph_incidence_queries() { - let graph = MixedGraph::new(4, vec![(0, 1), (2, 1)], vec![(1, 3), (0, 2)]); + let graph = MixedGraph::new(4, vec![(0, 1), (2, 1)], vec![(1, 3), (0, 2)]).unwrap(); assert!(graph.has_arc(0, 1)); assert!(!graph.has_arc(1, 0)); @@ -35,7 +35,7 @@ fn test_mixed_graph_incidence_queries() { #[test] fn test_mixed_graph_has_edge_is_order_insensitive() { - let graph = MixedGraph::new(3, vec![], vec![(2, 0)]); + let graph = MixedGraph::new(3, vec![], vec![(2, 0)]).unwrap(); assert!(graph.has_edge(0, 2)); assert!(graph.has_edge(2, 0)); @@ -43,7 +43,7 @@ fn test_mixed_graph_has_edge_is_order_insensitive() { #[test] fn test_mixed_graph_serialization_roundtrip() { - let graph = MixedGraph::new(5, vec![(0, 1), (1, 4)], vec![(0, 2), (2, 3), (3, 4)]); + let graph = MixedGraph::new(5, vec![(0, 1), (1, 4)], vec![(0, 2), (2, 3), (3, 4)]).unwrap(); let json = serde_json::to_string(&graph).unwrap(); let restored: MixedGraph = serde_json::from_str(&json).unwrap(); @@ -52,7 +52,16 @@ fn test_mixed_graph_serialization_roundtrip() { } #[test] -#[should_panic(expected = "references vertex >= num_vertices")] -fn test_mixed_graph_panics_on_out_of_bounds_arc() { - MixedGraph::new(3, vec![(0, 3)], vec![]); +fn test_mixed_graph_rejects_on_out_of_bounds_arc() { + assert!(MixedGraph::new(3, vec![(0, 3)], vec![]).is_err()); +} + +#[test] +fn deserialize_checks_both_arc_and_edge_endpoints() { + for (arcs, edges) in [(vec![(0, 2)], vec![]), (vec![], vec![(2, 0)])] { + assert!(serde_json::from_value::(serde_json::json!({ + "num_vertices": 2, "arcs": arcs, "edges": edges + })) + .is_err()); + } } diff --git a/src/unit_tests/topology/planar_graph.rs b/src/unit_tests/topology/planar_graph.rs index 7fae09cb6..224ec340f 100644 --- a/src/unit_tests/topology/planar_graph.rs +++ b/src/unit_tests/topology/planar_graph.rs @@ -4,14 +4,14 @@ use crate::topology::{Graph, PlanarGraph}; fn test_planar_graph_basic() { // K4 is planar: 4 vertices, 6 edges, 6 <= 3*4 - 6 = 6 let edges = vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]; - let g = PlanarGraph::new(4, edges); + let g = PlanarGraph::new(4, edges).unwrap(); assert_eq!(g.num_vertices(), 4); assert_eq!(g.num_edges(), 6); } #[test] fn test_planar_graph_delegates_to_inner() { - let g = PlanarGraph::new(3, vec![(0, 1), (1, 2)]); + let g = PlanarGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(); assert!(g.has_edge(0, 1)); assert!(!g.has_edge(0, 2)); let mut n1 = g.neighbors(1); @@ -20,7 +20,6 @@ fn test_planar_graph_delegates_to_inner() { } #[test] -#[should_panic] fn test_planar_graph_rejects_k5() { // K5 has 10 edges, but 3*5 - 6 = 9. Fails necessary condition. let mut edges = Vec::new(); @@ -29,18 +28,34 @@ fn test_planar_graph_rejects_k5() { edges.push((i, j)); } } - PlanarGraph::new(5, edges); + assert!(PlanarGraph::new(5, edges).is_err()); } #[test] fn test_planar_graph_empty() { - let g = PlanarGraph::new(3, vec![]); + let g = PlanarGraph::new(3, vec![]).unwrap(); assert_eq!(g.num_vertices(), 3); assert_eq!(g.num_edges(), 0); } #[test] fn test_planar_graph_tree() { - let g = PlanarGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let g = PlanarGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(); assert_eq!(g.num_edges(), 3); } + +#[test] +fn deserialize_checks_the_edge_bound() { + let edges: Vec<_> = (0..5) + .flat_map(|u| ((u + 1)..5).map(move |v| (u, v))) + .collect(); + assert!(serde_json::from_value::(serde_json::json!({ + "inner": {"num_vertices": 5, "edges": edges} + })) + .is_err()); + let graph: PlanarGraph = serde_json::from_value(serde_json::json!({ + "inner": {"num_vertices": 2, "edges": [[0, 1]]} + })) + .unwrap(); + assert_eq!(graph.num_edges(), 1); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 4f766da84..1a8911df1 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -8,7 +8,7 @@ use crate::topology::{BipartiteGraph, DirectedGraph, SimpleGraph}; use crate::variant::K3; fn check_brute_force_problem(problem: &P, name: &str) { - let dims = problem.dimensions(); + let dims = crate::solvers::cartesian_dimensions(&problem).unwrap(); assert!( !dims.is_empty() || name.contains("empty"), "{} should have dimensions", @@ -25,35 +25,35 @@ fn check_brute_force_problem(problem: &P, name: &str) { #[test] fn test_all_registered_brute_force_problems_define_dimensions() { check_brute_force_problem( - &MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]), + &MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]).unwrap(), "MaximumIndependentSet", ); check_brute_force_problem( - &MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]), + &MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]).unwrap(), "MinimumVertexCover", ); check_brute_force_problem( - &MaxCut::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64]), + &MaxCut::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64]).unwrap(), "MaxCut", ); check_brute_force_problem( - &KColoring::::new(SimpleGraph::new(3, vec![(0, 1)])), + &KColoring::::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap()), "KColoring", ); check_brute_force_problem( - &MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]), + &MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]).unwrap(), "MinimumDominatingSet", ); check_brute_force_problem( - &MaximalIS::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]), + &MaximalIS::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64; 3]).unwrap(), "MaximalIS", ); check_brute_force_problem( - &MaximumMatching::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64]), + &MaximumMatching::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![1i64]).unwrap(), "MaximumMatching", ); check_brute_force_problem( - &BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 3, 2)], 2), + &BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 3, 2)], 2).unwrap(), "BiconnectivityAugmentation", ); check_brute_force_problem( @@ -69,26 +69,26 @@ fn test_all_registered_brute_force_problems_define_dimensions() { "QUBO", ); check_brute_force_problem( - &MinimumSetCovering::new(3, vec![vec![0, 1]]), + &MinimumSetCovering::new(3, vec![vec![0, 1]]).unwrap(), "MinimumSetCovering", ); check_brute_force_problem( &MaximumSetPacking::new(vec![vec![0, 1]]), "MaximumSetPacking", ); - check_brute_force_problem(&PaintShop::new(vec!["a", "a"]), "PaintShop"); - check_brute_force_problem(&BMF::new(vec![vec![true]], 1), "BMF"); + check_brute_force_problem(&PaintShop::new(vec!["a", "a"]).unwrap(), "PaintShop"); + check_brute_force_problem(&BMF::new(vec![vec![true]], 1).unwrap(), "BMF"); check_brute_force_problem( - &ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2), + &ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2).unwrap(), "ConsecutiveBlockMinimization", ); check_brute_force_problem( - &BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0)]), 1), + &BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0)]).unwrap(), 1), "BicliqueCover", ); check_brute_force_problem( &BalancedCompleteBipartiteSubgraph::new( - BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]), + BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]).unwrap(), 2, ), "BalancedCompleteBipartiteSubgraph", @@ -96,7 +96,7 @@ fn test_all_registered_brute_force_problems_define_dimensions() { check_brute_force_problem(&Factoring::with_factor_bits(2, 2, 6), "Factoring"); check_brute_force_problem(&Partition::new(vec![3, 1, 1, 2, 2, 1]), "Partition").unwrap(); check_brute_force_problem( - &QuadraticAssignment::new(vec![vec![0, 1], vec![1, 0]], vec![vec![0, 1], vec![1, 0]]), + &QuadraticAssignment::new(vec![vec![0, 1], vec![1, 0]], vec![vec![0, 1], vec![1, 0]]).unwrap(), "QuadraticAssignment", ); @@ -107,7 +107,7 @@ fn test_all_registered_brute_force_problems_define_dimensions() { check_brute_force_problem(&CircuitSAT::new(circuit), "CircuitSAT"); check_brute_force_problem( &StrongConnectivityAugmentation::new( - DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]), + DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap(), vec![(0, 2, 1)], 1, ), @@ -115,57 +115,57 @@ fn test_all_registered_brute_force_problems_define_dimensions() { ); check_brute_force_problem( &KthBestSpanningTree::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1, 1, 1], 1, 2, - ), + ).unwrap(), "KthBestSpanningTree", ); check_brute_force_problem( - &HamiltonianCircuit::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)])), + &HamiltonianCircuit::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]).unwrap()), "HamiltonianCircuit", ); check_brute_force_problem( &MinMaxMulticenter::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1i64; 3], vec![1i64; 2], 1, - ), + ).unwrap(), "MinMaxMulticenter", ); check_brute_force_problem( - &HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])), + &HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()), "HamiltonianPath", ); check_brute_force_problem( - &DegreeConstrainedSpanningTree::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 2), + &DegreeConstrainedSpanningTree::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), 2).unwrap(), "DegreeConstrainedSpanningTree", ); check_brute_force_problem( &ShortestWeightConstrainedPath::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1i64; 2], vec![1i64; 2], 0, 2, 2, 2, - ), + ).unwrap(), "ShortestWeightConstrainedPath", ); check_brute_force_problem( &MultipleCopyFileAllocation::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), vec![1; 3], vec![1; 3], - ), + ).unwrap(), "MultipleCopyFileAllocation", ); check_brute_force_problem( &UndirectedTwoCommodityIntegralFlow::new( - SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]).unwrap(), vec![1, 1, 2], 0, 3, @@ -173,55 +173,55 @@ fn test_all_registered_brute_force_problems_define_dimensions() { 3, 1, 1, - ), + ).unwrap(), "UndirectedTwoCommodityIntegralFlow", ); check_brute_force_problem( &LengthBoundedDisjointPaths::new( - SimpleGraph::new(4, vec![(0, 1), (1, 3), (0, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 3), (0, 2), (2, 3)]).unwrap(), 0, 3, 2, 2, - ), + ).unwrap(), "LengthBoundedDisjointPaths", ); check_brute_force_problem( - &OptimalLinearArrangement::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])), + &OptimalLinearArrangement::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()), "OptimalLinearArrangement", ); check_brute_force_problem( &IsomorphicSpanningTree::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), ), "IsomorphicSpanningTree", ); check_brute_force_problem( - &ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]]), + &ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]]).unwrap(), "ShortestCommonSupersequence", ); check_brute_force_problem( - &FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10), + &FlowShopScheduling::new(2, vec![vec![1, 2], vec![3, 4]], 10).unwrap(), "FlowShopScheduling", ); check_brute_force_problem( - &JobShopScheduling::new(2, vec![vec![(0, 1), (1, 1)], vec![(1, 1), (0, 1)]], 2), + &JobShopScheduling::new(2, vec![vec![(0, 1), (1, 1)], vec![(1, 1), (0, 1)]], 2).unwrap(), "JobShopScheduling", ); check_brute_force_problem( - &SequencingToMinimizeWeightedTardiness::new(vec![3, 4, 2], vec![2, 3, 1], vec![5, 8, 4], 4), + &SequencingToMinimizeWeightedTardiness::new(vec![3, 4, 2], vec![2, 3, 1], vec![5, 8, 4], 4).unwrap(), "SequencingToMinimizeWeightedTardiness", ); check_brute_force_problem( - &MinimumTardinessSequencing::::new(3, vec![2, 3, 1], vec![(0, 2)]), + &MinimumTardinessSequencing::::new(3, vec![2, 3, 1], vec![(0, 2)]).unwrap(), "MinimumTardinessSequencing", ); check_brute_force_problem( &PartitionIntoPathsOfLength2::new(SimpleGraph::new( 6, vec![(0, 1), (1, 2), (3, 4), (4, 5)], - )), + ).unwrap()).unwrap(), "PartitionIntoPathsOfLength2", ); check_brute_force_problem( @@ -230,11 +230,11 @@ fn test_all_registered_brute_force_problems_define_dimensions() { "ResourceConstrainedScheduling", ); check_brute_force_problem( - &PartiallyOrderedKnapsack::new(vec![2, 3], vec![3, 2], vec![(0, 1)], 5), + &PartiallyOrderedKnapsack::new(vec![2, 3], vec![3, 2], vec![(0, 1)], 5).unwrap(), "PartiallyOrderedKnapsack", ); check_brute_force_problem( - &SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]), + &SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]).unwrap(), "SequencingWithReleaseTimesAndDeadlines", ); check_brute_force_problem( @@ -242,7 +242,7 @@ fn test_all_registered_brute_force_problems_define_dimensions() { "SumOfSquaresPartition", ); check_brute_force_problem( - &ConsecutiveOnesSubmatrix::new(vec![vec![true, false], vec![false, true]], 1), + &ConsecutiveOnesSubmatrix::new(vec![vec![true, false], vec![false, true]], 1).unwrap(), "ConsecutiveOnesSubmatrix", ); } diff --git a/src/unit_tests/traits.rs b/src/unit_tests/traits.rs index 2c6cec82d..2fb2e851c 100644 --- a/src/unit_tests/traits.rs +++ b/src/unit_tests/traits.rs @@ -13,7 +13,12 @@ impl Problem for TestSatProblem { type Solution = Vec; type Value = Or; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.num_vars as u64)]) + } fn evaluate( &self, @@ -28,8 +33,12 @@ impl Problem for TestSatProblem { } impl crate::solvers::BruteForceProblem for TestSatProblem { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -40,7 +49,10 @@ fn test_problem_sat() { satisfying: vec![vec![1, 0], vec![0, 1]], }; - assert_eq!(p.dimensions(), vec![2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2] + ); assert_eq!(p.evaluate(&vec![1, 0]).unwrap(), Or(true)); assert_eq!(p.evaluate(&vec![0, 0]).unwrap(), Or(false)); } @@ -52,8 +64,8 @@ fn test_problem_num_variables() { satisfying: vec![], }; - assert_eq!(p.num_variables(), 5); - assert_eq!(p.dimensions().len(), 5); + assert_eq!(p.num_variables().unwrap(), 5); + assert_eq!(crate::solvers::cartesian_dimensions(&p).unwrap().len(), 5); } #[test] @@ -63,8 +75,8 @@ fn test_problem_empty() { satisfying: vec![], }; - assert_eq!(p.num_variables(), 0); - assert!(p.dimensions().is_empty()); + assert_eq!(p.num_variables().unwrap(), 0); + assert!(crate::solvers::cartesian_dimensions(&p).unwrap().is_empty()); } #[derive(Clone)] @@ -77,7 +89,12 @@ impl Problem for TestMaxProblem { type Solution = Vec; type Value = Max; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.weights.len() as u64)]) + } fn evaluate( &self, @@ -99,12 +116,6 @@ impl Problem for TestMaxProblem { } } -impl crate::solvers::BruteForceProblem for TestMaxProblem { - fn dimensions(&self) -> Vec { - vec![2; self.weights.len()] - } -} - #[derive(Clone)] struct TestMinProblem { costs: Vec, @@ -115,7 +126,12 @@ impl Problem for TestMinProblem { type Solution = Vec; type Value = Min; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.costs.len() as u64)]) + } fn evaluate( &self, @@ -137,12 +153,6 @@ impl Problem for TestMinProblem { } } -impl crate::solvers::BruteForceProblem for TestMinProblem { - fn dimensions(&self) -> Vec { - vec![2; self.costs.len()] - } -} - #[test] fn test_problem_max_value() { let p = TestMaxProblem { @@ -175,7 +185,12 @@ impl Problem for MultiDimProblem { type Solution = Vec; type Value = Sum; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.dims.len() as u64)]) + } fn evaluate( &self, @@ -190,8 +205,12 @@ impl Problem for MultiDimProblem { } impl crate::solvers::BruteForceProblem for MultiDimProblem { - fn dimensions(&self) -> Vec { - self.dims.clone() + fn num_variables(&self) -> Result { + Ok(self.dims.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.dims[variable]) } } @@ -201,8 +220,11 @@ fn test_multi_dim_problem() { dims: vec![2, 3, 4], }; - assert_eq!(p.dimensions(), vec![2, 3, 4]); - assert_eq!(p.num_variables(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 3, 4] + ); + assert_eq!(p.num_variables().unwrap(), 3); assert_eq!(p.evaluate(&vec![0, 0, 0]).unwrap(), Sum(0)); assert_eq!(p.evaluate(&vec![1, 2, 3]).unwrap(), Sum(6)); } @@ -225,7 +247,12 @@ impl Problem for FloatProblem { type Solution = Vec; type Value = Max; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.weights.len() as u64)]) + } fn evaluate( &self, @@ -248,8 +275,12 @@ impl Problem for FloatProblem { } impl crate::solvers::BruteForceProblem for FloatProblem { - fn dimensions(&self) -> Vec { - vec![2; self.weights.len()] + fn num_variables(&self) -> Result { + Ok(self.weights.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -259,7 +290,10 @@ fn test_float_value_problem() { weights: vec![1.5, 2.5, 3.0], }; - assert_eq!(p.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] + ); assert!((p.evaluate(&vec![1, 1, 0]).unwrap().0.unwrap() - 4.0).abs() < 1e-10); assert!((p.evaluate(&vec![1, 1, 1]).unwrap().0.unwrap() - 7.0).abs() < 1e-10); } @@ -283,6 +317,9 @@ fn test_problem_is_clone() { }; let p2 = p1.clone(); - assert_eq!(p2.dimensions(), vec![2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p2).unwrap(), + vec![2, 2] + ); assert_eq!(p2.evaluate(&vec![1, 0]).unwrap(), Or(true)); } diff --git a/src/unit_tests/truth_table.rs b/src/unit_tests/truth_table.rs index 1685079ae..509305a1f 100644 --- a/src/unit_tests/truth_table.rs +++ b/src/unit_tests/truth_table.rs @@ -2,7 +2,7 @@ use super::*; #[test] fn test_and_gate() { - let and = TruthTable::and(2); + let and = TruthTable::and(2).unwrap(); assert!(!and.evaluate(&[false, false])); assert!(!and.evaluate(&[true, false])); assert!(!and.evaluate(&[false, true])); @@ -11,7 +11,7 @@ fn test_and_gate() { #[test] fn test_or_gate() { - let or = TruthTable::or(2); + let or = TruthTable::or(2).unwrap(); assert!(!or.evaluate(&[false, false])); assert!(or.evaluate(&[true, false])); assert!(or.evaluate(&[false, true])); @@ -27,7 +27,7 @@ fn test_not_gate() { #[test] fn test_xor_gate() { - let xor = TruthTable::xor(2); + let xor = TruthTable::xor(2).unwrap(); assert!(!xor.evaluate(&[false, false])); assert!(xor.evaluate(&[true, false])); assert!(xor.evaluate(&[false, true])); @@ -36,7 +36,7 @@ fn test_xor_gate() { #[test] fn test_nand_gate() { - let nand = TruthTable::nand(2); + let nand = TruthTable::nand(2).unwrap(); assert!(nand.evaluate(&[false, false])); assert!(nand.evaluate(&[true, false])); assert!(nand.evaluate(&[false, true])); @@ -54,7 +54,8 @@ fn test_implies() { #[test] fn test_from_function() { - let majority = TruthTable::from_function(3, |input| input.iter().filter(|&&b| b).count() >= 2); + let majority = + TruthTable::from_function(3, |input| input.iter().filter(|&&b| b).count() >= 2).unwrap(); assert!(!majority.evaluate(&[false, false, false])); assert!(!majority.evaluate(&[true, false, false])); assert!(majority.evaluate(&[true, true, false])); @@ -63,7 +64,7 @@ fn test_from_function() { #[test] fn test_evaluate_config() { - let and = TruthTable::and(2); + let and = TruthTable::and(2).unwrap(); assert!(!and.evaluate_config(&[0, 0])); assert!(!and.evaluate_config(&[1, 0])); assert!(and.evaluate_config(&[1, 1])); @@ -71,26 +72,26 @@ fn test_evaluate_config() { #[test] fn test_satisfiable() { - let or = TruthTable::or(2); + let or = TruthTable::or(2).unwrap(); assert!(or.is_satisfiable()); - let contradiction = TruthTable::from_outputs(2, vec![false, false, false, false]); + let contradiction = TruthTable::from_outputs(2, vec![false, false, false, false]).unwrap(); assert!(!contradiction.is_satisfiable()); assert!(contradiction.is_contradiction()); } #[test] fn test_tautology() { - let tautology = TruthTable::from_outputs(2, vec![true, true, true, true]); + let tautology = TruthTable::from_outputs(2, vec![true, true, true, true]).unwrap(); assert!(tautology.is_tautology()); - let or = TruthTable::or(2); + let or = TruthTable::or(2).unwrap(); assert!(!or.is_tautology()); } #[test] fn test_satisfying_assignments() { - let xor = TruthTable::xor(2); + let xor = TruthTable::xor(2).unwrap(); let sat = xor.satisfying_assignments(); assert_eq!(sat.len(), 2); assert!(sat.contains(&vec![true, false])); @@ -99,14 +100,14 @@ fn test_satisfying_assignments() { #[test] fn test_count() { - let and = TruthTable::and(2); + let and = TruthTable::and(2).unwrap(); assert_eq!(and.count_ones(), 1); assert_eq!(and.count_zeros(), 3); } #[test] fn test_index_to_input() { - let tt = TruthTable::and(3); + let tt = TruthTable::and(3).unwrap(); assert_eq!(tt.index_to_input(0), vec![false, false, false]); assert_eq!(tt.index_to_input(1), vec![true, false, false]); assert_eq!(tt.index_to_input(7), vec![true, true, true]); @@ -114,49 +115,49 @@ fn test_index_to_input() { #[test] fn test_outputs_vec() { - let and = TruthTable::and(2); + let and = TruthTable::and(2).unwrap(); assert_eq!(and.outputs_vec(), vec![false, false, false, true]); } #[test] fn test_and_with() { - let a = TruthTable::from_outputs(1, vec![false, true]); - let b = TruthTable::from_outputs(1, vec![true, false]); + let a = TruthTable::from_outputs(1, vec![false, true]).unwrap(); + let b = TruthTable::from_outputs(1, vec![true, false]).unwrap(); let result = a.and_with(&b); assert_eq!(result.outputs_vec(), vec![false, false]); } #[test] fn test_or_with() { - let a = TruthTable::from_outputs(1, vec![false, true]); - let b = TruthTable::from_outputs(1, vec![true, false]); + let a = TruthTable::from_outputs(1, vec![false, true]).unwrap(); + let b = TruthTable::from_outputs(1, vec![true, false]).unwrap(); let result = a.or_with(&b); assert_eq!(result.outputs_vec(), vec![true, true]); } #[test] fn test_negate() { - let and = TruthTable::and(2); + let and = TruthTable::and(2).unwrap(); let nand = and.negate(); assert_eq!(nand.outputs_vec(), vec![true, true, true, false]); } #[test] fn test_num_rows() { - let tt = TruthTable::and(3); + let tt = TruthTable::and(3).unwrap(); assert_eq!(tt.num_rows(), 8); } #[test] fn test_3_input_and() { - let and3 = TruthTable::and(3); + let and3 = TruthTable::and(3).unwrap(); assert!(!and3.evaluate(&[true, true, false])); assert!(and3.evaluate(&[true, true, true])); } #[test] fn test_xnor() { - let xnor = TruthTable::xnor(2); + let xnor = TruthTable::xnor(2).unwrap(); assert!(xnor.evaluate(&[false, false])); assert!(!xnor.evaluate(&[true, false])); assert!(!xnor.evaluate(&[false, true])); @@ -165,7 +166,7 @@ fn test_xnor() { #[test] fn test_nor() { - let nor = TruthTable::nor(2); + let nor = TruthTable::nor(2).unwrap(); assert!(nor.evaluate(&[false, false])); assert!(!nor.evaluate(&[true, false])); assert!(!nor.evaluate(&[false, true])); @@ -174,7 +175,7 @@ fn test_nor() { #[test] fn test_serialization() { - let and = TruthTable::and(2); + let and = TruthTable::and(2).unwrap(); let json = serde_json::to_string(&and).unwrap(); let deserialized: TruthTable = serde_json::from_str(&json).unwrap(); assert_eq!(and, deserialized); @@ -182,13 +183,37 @@ fn test_serialization() { #[test] fn test_outputs() { - let and = TruthTable::and(2); + let and = TruthTable::and(2).unwrap(); let outputs = and.outputs(); assert_eq!(outputs.len(), 4); } #[test] fn test_num_inputs() { - let and = TruthTable::and(3); + let and = TruthTable::and(3).unwrap(); assert_eq!(and.num_inputs(), 3); } + +#[test] +fn construction_and_deserialization_enforce_row_shape() { + for outputs in [vec![true], vec![false; 5]] { + assert!(TruthTable::from_outputs(2, outputs.clone()).is_err()); + assert!(serde_json::from_value::(serde_json::json!({ + "num_inputs": 2, "outputs": outputs + })) + .is_err()); + } + let inputs = usize::BITS as usize; + assert!(matches!( + TruthTable::from_outputs(inputs, vec![]), + Err(ConstructionError::IntegerOverflow(_)) + )); + assert!(TruthTable::from_function(inputs, |_| true).is_err()); + assert!(serde_json::from_value::(serde_json::json!({ + "num_inputs": inputs, "outputs": [] + })) + .is_err()); + let empty = TruthTable::from_outputs(0, vec![true]).unwrap(); + assert_eq!(empty.num_rows(), 1); + assert!(empty.evaluate(&[])); +} diff --git a/src/unit_tests/types.rs b/src/unit_tests/types.rs index ac53023b0..de88e9934 100644 --- a/src/unit_tests/types.rs +++ b/src/unit_tests/types.rs @@ -1,6 +1,6 @@ use super::*; use crate::traits::EvaluationError; -use crate::types::{Aggregate, SolutionAggregate}; +use crate::types::Aggregate; #[test] fn test_max_identity_and_combine() { @@ -96,27 +96,6 @@ fn test_and_absorbing_value_is_false() { assert!(And(false).is_absorbing()); } -#[test] -fn test_max_solution_selection() { - assert!(Max::contributes_to_solution(&Max(Some(7)), &Max(Some(7)))); - assert!(!Max::contributes_to_solution(&Max(Some(3)), &Max(Some(7)))); - assert!(!Max::contributes_to_solution(&Max(None), &Max(Some(7)))); -} - -#[test] -fn test_min_solution_selection() { - assert!(Min::contributes_to_solution(&Min(Some(3)), &Min(Some(3)))); - assert!(!Min::contributes_to_solution(&Min(Some(7)), &Min(Some(3)))); - assert!(!Min::contributes_to_solution(&Min(None), &Min(Some(3)))); -} - -#[test] -fn test_or_solution_selection() { - assert!(Or::contributes_to_solution(&Or(true), &Or(true))); - assert!(!Or::contributes_to_solution(&Or(false), &Or(true))); - assert!(!Or::contributes_to_solution(&Or(true), &Or(false))); -} - #[test] fn test_max_helpers() { let size = Max(Some(42)); @@ -332,27 +311,6 @@ fn test_extremum_aggregate_identity_and_combine() { assert_eq!(combined, Extremum::minimize(Some(3))); } -#[test] -fn test_extremum_solution_selection() { - // Matching value and sense -> contributes - assert!(Extremum::contributes_to_solution( - &Extremum::maximize(Some(10)), - &Extremum::maximize(Some(10)), - )); - - // Different value -> does not contribute - assert!(!Extremum::contributes_to_solution( - &Extremum::maximize(Some(5)), - &Extremum::maximize(Some(10)), - )); - - // None config -> does not contribute - assert!(!Extremum::contributes_to_solution( - &Extremum::::maximize(None), - &Extremum::maximize(Some(10)), - )); -} - #[test] fn test_extremum_display() { assert_eq!(format!("{}", Extremum::maximize(Some(42))), "Max(42)"); diff --git a/tests/suites/integration.rs b/tests/suites/integration.rs index db976e33f..ee739eddb 100644 --- a/tests/suites/integration.rs +++ b/tests/suites/integration.rs @@ -19,9 +19,10 @@ mod all_problems_solvable { #[test] fn test_independent_set_solvable() { let problem = MaximumIndependentSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -33,9 +34,10 @@ mod all_problems_solvable { #[test] fn test_vertex_covering_solvable() { let problem = MinimumVertexCover::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -47,9 +49,10 @@ mod all_problems_solvable { #[test] fn test_max_cut_solvable() { let problem = MaxCut::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1, 2, 1], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -57,7 +60,7 @@ mod all_problems_solvable { #[test] fn test_coloring_solvable() { - let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap()); let solver = BruteForce::new(); // KColoring uses the witness-capable `Or` aggregate, so all witnesses are valid colorings. let satisfying = solver.find_all_witnesses(&problem).unwrap(); @@ -70,9 +73,10 @@ mod all_problems_solvable { #[test] fn test_dominating_set_solvable() { let problem = MinimumDominatingSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -84,9 +88,10 @@ mod all_problems_solvable { #[test] fn test_maximal_is_solvable() { let problem = MaximalIS::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -98,9 +103,10 @@ mod all_problems_solvable { #[test] fn test_matching_solvable() { let problem = MaximumMatching::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1, 2, 1], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -124,13 +130,15 @@ mod all_problems_solvable { (4, 5), (1, 4), ], - ), + ) + .unwrap(), vec![2, 4, 3, 1, 5, 4, 2, 6], vec![5, 1, 2, 3, 2, 3, 1, 1], 0, 5, 8, - ); + ) + .unwrap(); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap(); assert!(solution.is_some()); @@ -143,7 +151,8 @@ mod all_problems_solvable { SimpleGraph::path(4), vec![(0, 2, 5), (1, 3, 1), (0, 3, 2)], 2, - ); + ) + .unwrap(); let solver = BruteForce::new(); let satisfying = solver.find_all_witnesses(&problem).unwrap(); assert_eq!(satisfying, vec![vec![false, false, true]]); @@ -190,7 +199,8 @@ mod all_problems_solvable { #[test] fn test_set_covering_solvable() { let problem = - MinimumSetCovering::::new(5, vec![vec![0, 1, 2], vec![2, 3, 4], vec![0, 4]]); + MinimumSetCovering::::new(5, vec![vec![0, 1, 2], vec![2, 3, 4], vec![0, 4]]) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -242,7 +252,8 @@ mod all_problems_solvable { 3, vec![(vec![0], vec![1])], vec![0, 1, 2], - ); + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(solutions.contains(&vec![true, false, false])); @@ -250,7 +261,7 @@ mod all_problems_solvable { #[test] fn test_paintshop_solvable() { - let problem = PaintShop::new(vec!["a", "b", "a", "b"]); + let problem = PaintShop::new(vec!["a", "b", "a", "b"]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -260,7 +271,7 @@ mod all_problems_solvable { fn test_biclique_cover_solvable() { // Left vertices: 0, 1; Right vertices: 2, 3 let problem = BicliqueCover::new( - BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]), + BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]).unwrap(), 1, ); let solver = BruteForce::new(); @@ -274,7 +285,7 @@ mod all_problems_solvable { #[test] fn test_bmf_solvable() { // All-ones 2x2 at rank 1 has an exact boolean factorization. - let problem = BMF::new(vec![vec![true, true], vec![true, true]], 1); + let problem = BMF::new(vec![vec![true, true], vec![true, true]], 1).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(!solutions.is_empty()); @@ -297,8 +308,10 @@ mod problem_relationships { let n = 4; let is_problem = - MaximumIndependentSet::new(SimpleGraph::new(n, edges.clone()), vec![1i64; n]); - let vc_problem = MinimumVertexCover::new(SimpleGraph::new(n, edges), vec![1i64; n]); + MaximumIndependentSet::new(SimpleGraph::new(n, edges.clone()).unwrap(), vec![1i64; n]) + .unwrap(); + let vc_problem = + MinimumVertexCover::new(SimpleGraph::new(n, edges).unwrap(), vec![1i64; n]).unwrap(); let solver = BruteForce::new(); let is_solutions = solver.find_all_witnesses(&is_problem).unwrap(); @@ -317,8 +330,10 @@ mod problem_relationships { let edges = vec![(0, 1), (1, 2), (2, 3)]; let n = 4; - let maximal_is = MaximalIS::new(SimpleGraph::new(n, edges.clone()), vec![1i64; n]); - let is_problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges), vec![1i64; n]); + let maximal_is = + MaximalIS::new(SimpleGraph::new(n, edges.clone()).unwrap(), vec![1i64; n]).unwrap(); + let is_problem = + MaximumIndependentSet::new(SimpleGraph::new(n, edges).unwrap(), vec![1i64; n]).unwrap(); let solver = BruteForce::new(); let maximal_solutions = solver.find_all_witnesses(&maximal_is).unwrap(); @@ -376,7 +391,7 @@ mod problem_relationships { // Three disjoint sets covering universe {0,1,2,3,4,5} let sets = vec![vec![0, 1], vec![2, 3], vec![4, 5]]; - let covering = MinimumSetCovering::::new(6, sets.clone()); + let covering = MinimumSetCovering::::new(6, sets.clone()).unwrap(); let packing = MaximumSetPacking::::new(sets); let solver = BruteForce::new(); @@ -409,7 +424,9 @@ mod edge_cases { #[test] fn test_empty_graph_independent_set() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![]), vec![1i64; 3]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![]).unwrap(), vec![1i64; 3]) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -421,7 +438,8 @@ mod edge_cases { fn test_complete_graph_independent_set() { // K4 - complete graph on 4 vertices let edges = vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]; - let problem = MaximumIndependentSet::new(SimpleGraph::new(4, edges), vec![1i64; 4]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(4, edges).unwrap(), vec![1i64; 4]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -456,7 +474,7 @@ mod edge_cases { #[test] fn test_single_car_paintshop() { - let problem = PaintShop::new(vec!["a", "a"]); + let problem = PaintShop::new(vec!["a", "a"]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -471,7 +489,9 @@ mod weighted_problems { #[test] fn test_weighted_independent_set() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![10, 1, 1]); + let problem = + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![10, 1, 1]) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -488,8 +508,11 @@ mod weighted_problems { #[test] fn test_weighted_vertex_cover() { - let problem = - MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 10, 1]); + let problem = MinimumVertexCover::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![1, 10, 1], + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -505,7 +528,11 @@ mod weighted_problems { #[test] fn test_weighted_max_cut() { - let problem = MaxCut::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![10, 1]); + let problem = MaxCut::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]).unwrap(), + vec![10, 1], + ) + .unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -532,3 +559,52 @@ mod weighted_problems { assert!(satisfying.is_empty()); } } + +/// Exercise the solver as a downstream crate: struct construction, generic +/// bounds, solution type, and exhaustive error matching must keep compiling. +#[test] +fn ilp_public_api_supports_generic_witness_solving() { + use problemreductions::solvers::{ILPSolveError, ILPSolver}; + fn solve_generic

(problem: &P) -> std::result::Result + where + P: Problem + 'static, + P::Solution: 'static, + P::Value: problemreductions::solvers::SolutionAggregate, + { + ILPSolver::new().solve(problem) + } + fn classify_error(error: ILPSolveError) -> &'static str { + match error { + ILPSolveError::Infeasible => "infeasible", + ILPSolveError::Unbounded => "unbounded", + ILPSolveError::Timeout => "timeout", + ILPSolveError::BackendFailure(_) => "backend", + ILPSolveError::UnsupportedProblemType => "unsupported", + ILPSolveError::MissingPipeline(_) => "missing pipeline", + ILPSolveError::InvalidRegistry(_) => "registry", + ILPSolveError::PipelineTypeMismatch(_) => "type mismatch", + ILPSolveError::InvalidSolution(_) => "invalid solution", + ILPSolveError::Evaluation(_) => "evaluation", + ILPSolveError::InexactTransport(_) => "transport", + ILPSolveError::Extraction(_) => "extraction", + ILPSolveError::Reduction(_) => "reduction", + } + } + let solver = ILPSolver { time_limit: None }; + let ILPSolver { time_limit } = solver.clone(); + assert_eq!(time_limit, None); + let ilp = ILP::::new(1, vec![], vec![(0, 1)], ObjectiveSense::Maximize).unwrap(); + let solution: Vec = solve_generic(&ilp).unwrap(); + assert_eq!(solution, vec![1]); + let infeasible = ILP::::new( + 0, + vec![LinearConstraint::ge(vec![], 1)], + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(); + assert_eq!( + classify_error(solver.solve(&infeasible).unwrap_err()), + "infeasible" + ); +} diff --git a/tests/suites/ksatisfiability_simultaneous_incongruences.rs b/tests/suites/ksatisfiability_simultaneous_incongruences.rs index 947f81dab..09cdbeb98 100644 --- a/tests/suites/ksatisfiability_simultaneous_incongruences.rs +++ b/tests/suites/ksatisfiability_simultaneous_incongruences.rs @@ -19,7 +19,7 @@ fn test_ksatisfiability_to_simultaneous_incongruences_closed_loop() { .expect("reduction should succeed"); let target = reduction.target_problem(); - assert_eq!(target.lcm_moduli(), 105); + assert_eq!(target.lcm_moduli().unwrap(), 105); assert_eq!(target.num_pairs(), 11); let solver = BruteForce::new(); diff --git a/tests/suites/numeric_boundaries.rs b/tests/suites/numeric_boundaries.rs index ae98948d3..5dbe72f53 100644 --- a/tests/suites/numeric_boundaries.rs +++ b/tests/suites/numeric_boundaries.rs @@ -14,20 +14,22 @@ use problemreductions::Problem; fn numeric_boundaries_weight_totals_use_i64() { let weight = i64::MAX / 2; let expected = i64::MAX - 1; - let dominating = MinimumDominatingSet::new(SimpleGraph::new(2, vec![]), vec![weight, weight]); + let dominating = + MinimumDominatingSet::new(SimpleGraph::new(2, vec![]).unwrap(), vec![weight, weight]) + .unwrap(); assert_eq!( dominating.evaluate(&vec![true, true]).unwrap().0, Some(expected) ); let covering = - MinimumSetCovering::with_weights(2, vec![vec![0], vec![1]], vec![weight, weight]); + MinimumSetCovering::with_weights(2, vec![vec![0], vec![1]], vec![weight, weight]).unwrap(); assert_eq!( covering.evaluate(&vec![true, true]).unwrap().0, Some(expected) ); - let ordinary = MinimumSetCovering::with_weights(1, vec![vec![0]], vec![7i64]); + let ordinary = MinimumSetCovering::with_weights(1, vec![vec![0]], vec![7i64]).unwrap(); assert_eq!(ordinary.evaluate(&vec![true]).unwrap().0, Some(7)); } diff --git a/tests/suites/reductions.rs b/tests/suites/reductions.rs index 51f4746e4..5eb3fcfd4 100644 --- a/tests/suites/reductions.rs +++ b/tests/suites/reductions.rs @@ -21,9 +21,10 @@ mod is_vc_reductions { fn test_is_to_vc_basic() { // Triangle graph let is_problem = MaximumIndependentSet::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); // Reduce IS to VC let result = ReduceTo::>::reduce_to(&is_problem) @@ -49,9 +50,10 @@ mod is_vc_reductions { fn test_vc_to_is_basic() { // Path graph let vc_problem = MinimumVertexCover::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); // Reduce VC to IS let result = ReduceTo::>::reduce_to(&vc_problem) @@ -76,9 +78,10 @@ mod is_vc_reductions { #[test] fn test_is_vc_roundtrip() { let original = MaximumIndependentSet::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap(), vec![1i64; 5], - ); + ) + .unwrap(); // IS -> VC let to_vc = ReduceTo::>::reduce_to(&original) @@ -112,7 +115,8 @@ mod is_vc_reductions { #[test] fn test_is_vc_weighted() { let is_problem = - MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![10, 1, 5]); + MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), vec![10, 1, 5]) + .unwrap(); let result = ReduceTo::>::reduce_to(&is_problem) .expect("reduction should succeed"); @@ -129,8 +133,10 @@ mod is_vc_reductions { let n = 4; let is_problem = - MaximumIndependentSet::new(SimpleGraph::new(n, edges.clone()), vec![1i64; n]); - let vc_problem = MinimumVertexCover::new(SimpleGraph::new(n, edges), vec![1i64; n]); + MaximumIndependentSet::new(SimpleGraph::new(n, edges.clone()).unwrap(), vec![1i64; n]) + .unwrap(); + let vc_problem = + MinimumVertexCover::new(SimpleGraph::new(n, edges).unwrap(), vec![1i64; n]).unwrap(); let solver = BruteForce::new(); @@ -153,9 +159,10 @@ mod is_sp_reductions { fn test_is_to_sp_basic() { // Triangle graph - each vertex's incident edges become a set let is_problem = MaximumIndependentSet::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![1i64; 3], - ); + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&is_problem) .expect("reduction should succeed"); @@ -202,9 +209,10 @@ mod is_sp_reductions { #[test] fn test_is_sp_roundtrip() { let original = MaximumIndependentSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); // IS -> SP let to_sp = ReduceTo::>::reduce_to(&original) @@ -246,7 +254,7 @@ mod sg_qubo_reductions { let result = ReduceTo::>::reduce_to(&sg).expect("reduction should succeed"); let qubo = result.target_problem(); - assert_eq!(qubo.num_variables(), 2); + assert_eq!(qubo.num_variables().unwrap(), 2); // Solve QUBO let solver = BruteForce::new(); @@ -314,8 +322,9 @@ mod minimum_covering_by_cliques_ilp_reductions { #[test] fn test_covering_by_cliques_to_ilp_closed_loop() { - let source = - MinimumCoveringByCliques::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); + let source = MinimumCoveringByCliques::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), + ); let reduction = as ReduceTo>>::reduce_to(&source) @@ -337,10 +346,7 @@ mod partition_into_cliques_covering_by_cliques_reductions { #[test] fn test_partition_into_cliques_to_covering_by_cliques_closed_loop() { - let source: PartitionIntoCliques = serde_json::from_value(serde_json::json!({ - "graph": {"num_vertices": 0, "edges": []}, "num_cliques": 0 - })) - .unwrap(); + let source = PartitionIntoCliques::new(SimpleGraph::empty(1), 1).unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); @@ -357,7 +363,8 @@ mod partition_into_cliques_covering_by_cliques_reductions { #[test] fn test_partition_into_cliques_to_covering_by_cliques_orlin_issue_counts() { - let source = PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1)]), 2); + let source = + PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1)]).unwrap(), 2).unwrap(); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -431,9 +438,10 @@ mod sg_maxcut_reductions { #[test] fn test_maxcut_to_sg_basic() { let maxcut = MaxCut::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]).unwrap(), vec![2, 1, 3], - ); + ) + .unwrap(); let result = ReduceTo::>::reduce_to(&maxcut) .expect("reduction should succeed"); @@ -510,7 +518,8 @@ mod topology_tests { // Extract edges let edges = udg.edges().to_vec(); - let is_problem = MaximumIndependentSet::new(SimpleGraph::new(4, edges), vec![1i64; 4]); + let is_problem = + MaximumIndependentSet::new(SimpleGraph::new(4, edges).unwrap(), vec![1i64; 4]).unwrap(); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&is_problem).unwrap(); @@ -556,7 +565,11 @@ mod qubo_reductions { let data: ISToQuboData = serde_json::from_str(&json).unwrap(); let n = data.source.num_vertices; - let is = MaximumIndependentSet::new(SimpleGraph::new(n, data.source.edges), vec![1i64; n]); + let is = MaximumIndependentSet::new( + SimpleGraph::new(n, data.source.edges).unwrap(), + vec![1i64; n], + ) + .unwrap(); let graph = ReductionGraph::new(); let src = ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); @@ -574,7 +587,7 @@ mod qubo_reductions { .expect("Should reduce MaximumIndependentSet to QUBO"); let qubo: &QUBO = chain.target_problem(); - assert_eq!(qubo.num_variables(), data.qubo_num_vars); + assert_eq!(qubo.num_variables().unwrap(), data.qubo_num_vars); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -613,14 +626,13 @@ mod qubo_reductions { assert_eq!(data.source.num_colors, 3); - let kc = KColoring::::new(SimpleGraph::new( - data.source.num_vertices, - data.source.edges, - )); + let kc = KColoring::::new( + SimpleGraph::new(data.source.num_vertices, data.source.edges).unwrap(), + ); let reduction = ReduceTo::::reduce_to(&kc).expect("reduction should succeed"); let qubo = reduction.target_problem(); - assert_eq!(qubo.num_variables(), data.qubo_num_vars); + assert_eq!(qubo.num_variables().unwrap(), data.qubo_num_vars); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -657,7 +669,7 @@ mod qubo_reductions { let reduction = ReduceTo::>::reduce_to(&sp).expect("reduction should succeed"); let qubo = reduction.target_problem(); - assert_eq!(qubo.num_variables(), data.qubo_num_vars); + assert_eq!(qubo.num_variables().unwrap(), data.qubo_num_vars); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -727,7 +739,7 @@ mod qubo_reductions { let reduction = ReduceTo::::reduce_to(&ksat).expect("reduction should succeed"); let qubo = reduction.target_problem(); - assert_eq!(qubo.num_variables(), data.qubo_num_vars); + assert_eq!(qubo.num_variables().unwrap(), data.qubo_num_vars); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -815,7 +827,7 @@ mod qubo_reductions { let qubo = reduction.target_problem(); // QUBO may have more variables (slack), but original count matches - assert!(qubo.num_variables() >= data.qubo_num_vars); + assert!(qubo.num_variables().unwrap() >= data.qubo_num_vars); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -856,7 +868,11 @@ mod qubo_reductions { let data: VCToQuboData = serde_json::from_str(&json).unwrap(); let n = data.source.num_vertices; - let vc = MinimumVertexCover::new(SimpleGraph::new(n, data.source.edges), vec![1i64; n]); + let vc = MinimumVertexCover::new( + SimpleGraph::new(n, data.source.edges).unwrap(), + vec![1i64; n], + ) + .unwrap(); // Find path MVC → ... → QUBO through the reduction graph let graph = ReductionGraph::new(); @@ -917,9 +933,10 @@ mod io_tests { #[test] fn test_serialize_reduce_deserialize() { let original = MaximumIndependentSet::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]).unwrap(), vec![1i64; 4], - ); + ) + .unwrap(); // Serialize let json = to_json(&original).unwrap(); @@ -977,9 +994,10 @@ mod end_to_end { fn test_full_pipeline_is_vc_sp() { // Start with an MaximumIndependentSet problem let is = MaximumIndependentSet::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4)]), + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4)]).unwrap(), vec![1i64; 5], - ); + ) + .unwrap(); // Solve directly let solver = BruteForce::new(); diff --git a/tests/suites/simultaneous_incongruences.rs b/tests/suites/simultaneous_incongruences.rs index 6c4aa63c0..7306e27e5 100644 --- a/tests/suites/simultaneous_incongruences.rs +++ b/tests/suites/simultaneous_incongruences.rs @@ -1,6 +1,5 @@ use problemreductions::models::algebraic::SimultaneousIncongruences; use problemreductions::solvers::BruteForce; -use problemreductions::solvers::BruteForceProblem as _; use problemreductions::traits::Problem; #[test] @@ -10,8 +9,11 @@ fn test_simultaneous_incongruences_issue_example() { assert_eq!(problem.num_pairs(), 4); assert_eq!(problem.pairs(), &[(2, 2), (1, 3), (2, 5), (3, 7)]); - assert_eq!(problem.lcm_moduli(), 210); - assert_eq!(problem.dimensions(), vec![210]); + assert_eq!(problem.lcm_moduli().unwrap(), 210); + assert_eq!( + problemreductions::solvers::cartesian_dimensions(&problem).unwrap(), + vec![210] + ); // x=5: 5%2=1!=0(=2%2), 5%3=2!=1, 5%5=0!=2, 5%7=5!=3 => valid assert!(problem.evaluate(&5).unwrap()); // x=2: 2%2=0=2%2 => invalid (first incongruence violated)