From e55ea916109489b1e9942e85e7b167ea02d7163e Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Sun, 27 Sep 2026 09:48:23 -0700 Subject: [PATCH 1/5] Add explicit bounded integer ILP variants and migrate reduction rules --- .claude/CLAUDE.md | 2 +- docs/paper/reductions.typ | 8 +- docs/src/design.md | 30 +- ...hained_reduction_factoring_to_spinglass.rs | 6 +- problemreductions-cli/src/create_args.rs | 29 +- problemreductions-cli/src/dispatch.rs | 2 +- problemreductions-cli/src/problem_name.rs | 37 +- problemreductions-cli/tests/cli_tests.rs | 12 +- src/example_db/specs.rs | 44 ++- src/models/algebraic/ilp.rs | 117 ++++-- src/models/algebraic/mod.rs | 4 +- src/rules/acyclicpartition_ilp.rs | 16 +- src/rules/biconnectivityaugmentation_ilp.rs | 16 +- src/rules/bottlenecktravelingsalesman_ilp.rs | 16 +- .../boundedcomponentspanningforest_ilp.rs | 16 +- src/rules/closeststring_ilp.rs | 16 +- src/rules/closestsubstring_ilp.rs | 16 +- .../directedtwocommodityintegralflow_ilp.rs | 16 +- src/rules/ensemblecomputation_ilp.rs | 12 +- src/rules/eulerianpath_ilp.rs | 14 +- src/rules/factoring_ilp.rs | 30 +- src/rules/feasibleregisterassignment_ilp.rs | 12 +- src/rules/flowshopscheduling_ilp.rs | 23 +- src/rules/ilp_bool_ilp_i64.rs | 15 +- src/rules/ilp_bounded_ilp.rs | 58 +++ src/rules/ilp_i64_ilp_bool.rs | 20 +- src/rules/integerknapsack_ilp.rs | 14 +- src/rules/integralflowbundles_ilp.rs | 12 +- src/rules/integralflowhomologousarcs_ilp.rs | 12 +- src/rules/integralflowwithmultipliers_ilp.rs | 12 +- ...tisfiability_feasibleregisterassignment.rs | 11 +- src/rules/longestpath_ilp.rs | 14 +- src/rules/maximumleafspanningtree_ilp.rs | 12 +- .../minimumcapacitatedspanningtree_ilp.rs | 16 +- src/rules/minimumedgecostflow_ilp.rs | 16 +- src/rules/minimumfeedbackarcset_ilp.rs | 14 +- src/rules/minimumfeedbackvertexset_ilp.rs | 23 +- src/rules/minimumgraphbandwidth_ilp.rs | 16 +- src/rules/minimumweightdecoding_ilp.rs | 16 +- src/rules/minmaxmulticenter_ilp.rs | 20 +- src/rules/mixedchinesepostman_ilp.rs | 28 +- src/rules/mod.rs | 1 + src/rules/multiplechoicebranching_ilp.rs | 14 +- src/rules/openshopscheduling_ilp.rs | 33 +- src/rules/optimallineararrangement_ilp.rs | 28 +- src/rules/pathconstrainednetworkflow_ilp.rs | 12 +- src/rules/preemptivescheduling_ilp.rs | 20 +- src/rules/registersufficiency_ilp.rs | 14 +- src/rules/rootedtreestorageassignment_ilp.rs | 15 +- src/rules/ruralpostman_ilp.rs | 12 +- ...ingtominimizeweightedcompletiontime_ilp.rs | 18 +- ...cingtominimizemaximumcumulativecost_ilp.rs | 22 +- ...ingtominimizeweightedcompletiontime_ilp.rs | 16 +- ...quencingtominimizeweightedtardiness_ilp.rs | 18 +- .../shortestweightconstrainedpath_ilp.rs | 16 +- .../strongconnectivityaugmentation_ilp.rs | 14 +- src/rules/undirectedflowlowerbounds_ilp.rs | 16 +- .../undirectedtwocommodityintegralflow_ilp.rs | 21 +- src/solvers/ilp/adapter.rs | 16 +- src/solvers/ilp/solver.rs | 5 +- src/solvers/pipelines.rs | 346 +++++++++--------- src/solvers/registry.rs | 13 +- src/unit_tests/example_db.rs | 2 + src/unit_tests/models/algebraic/ilp.rs | 43 ++- .../parameter_formula_validation.rs | 10 +- src/unit_tests/reduction_graph.rs | 25 ++ src/unit_tests/rules/acyclicpartition_ilp.rs | 16 +- src/unit_tests/rules/aggregate_contracts.rs | 3 +- .../rules/biconnectivityaugmentation_ilp.rs | 16 +- .../rules/bottlenecktravelingsalesman_ilp.rs | 18 +- .../boundedcomponentspanningforest_ilp.rs | 11 +- src/unit_tests/rules/closeststring_ilp.rs | 23 +- src/unit_tests/rules/closestsubstring_ilp.rs | 23 +- .../directedtwocommodityintegralflow_ilp.rs | 18 +- .../rules/ensemblecomputation_ilp.rs | 10 +- src/unit_tests/rules/eulerianpath_ilp.rs | 17 +- src/unit_tests/rules/factoring_ilp.rs | 39 +- .../rules/feasibleregisterassignment_ilp.rs | 13 +- .../rules/flowshopscheduling_ilp.rs | 17 +- src/unit_tests/rules/ilp_bool_ilp_i64.rs | 11 +- src/unit_tests/rules/ilp_bounded_ilp.rs | 40 ++ src/unit_tests/rules/ilp_i64_ilp_bool.rs | 8 +- src/unit_tests/rules/integerknapsack_ilp.rs | 11 +- .../rules/integralflowbundles_ilp.rs | 14 +- .../rules/integralflowhomologousarcs_ilp.rs | 7 +- .../rules/integralflowwithmultipliers_ilp.rs | 7 +- ...bility_directedtwocommodityintegralflow.rs | 4 +- ...tisfiability_feasibleregisterassignment.rs | 5 +- src/unit_tests/rules/longestpath_ilp.rs | 12 +- .../rules/maximumleafspanningtree_ilp.rs | 20 +- .../minimumcapacitatedspanningtree_ilp.rs | 18 +- .../rules/minimumedgecostflow_ilp.rs | 14 +- .../rules/minimumfeedbackarcset_ilp.rs | 11 +- .../rules/minimumfeedbackvertexset_ilp.rs | 19 +- .../rules/minimumgraphbandwidth_ilp.rs | 11 +- .../rules/minimumweightdecoding_ilp.rs | 14 +- src/unit_tests/rules/minmaxmulticenter_ilp.rs | 12 +- .../rules/mixedchinesepostman_ilp.rs | 12 +- .../rules/multiplechoicebranching_ilp.rs | 9 +- .../rules/openshopscheduling_ilp.rs | 17 +- .../rules/optimallineararrangement_ilp.rs | 11 +- .../rules/partition_openshopscheduling.rs | 4 +- .../rules/pathconstrainednetworkflow_ilp.rs | 7 +- .../rules/preemptivescheduling_ilp.rs | 13 +- src/unit_tests/rules/reduction_path_parity.rs | 4 +- .../rules/registersufficiency_ilp.rs | 16 +- .../rules/rootedtreestorageassignment_ilp.rs | 10 +- src/unit_tests/rules/ruralpostman_ilp.rs | 12 +- ...ingtominimizeweightedcompletiontime_ilp.rs | 14 +- ...cingtominimizemaximumcumulativecost_ilp.rs | 10 +- ...ingtominimizeweightedcompletiontime_ilp.rs | 24 +- ...quencingtominimizeweightedtardiness_ilp.rs | 15 +- .../shortestweightconstrainedpath_ilp.rs | 10 +- .../strongconnectivityaugmentation_ilp.rs | 12 +- .../rules/undirectedflowlowerbounds_ilp.rs | 14 +- .../undirectedtwocommodityintegralflow_ilp.rs | 20 +- src/unit_tests/solvers/registry.rs | 35 +- src/unit_tests/solvers/resolver.rs | 10 +- .../symbolic_parameter_contracts.rs | 13 +- .../suites/register_assignment_reductions.rs | 8 +- 120 files changed, 1399 insertions(+), 936 deletions(-) create mode 100644 src/rules/ilp_bounded_ilp.rs create mode 100644 src/unit_tests/rules/ilp_bounded_ilp.rs diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 86e96ad3d..677dee23b 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -194,7 +194,7 @@ impl ReduceTo for Source { ... } - Every target parameter must appear exactly once as a formula or as unavailable with a non-empty reason. - `ParameterTransform` evaluates and composes formulas with exact rational and arbitrary-precision integer arithmetic. Composition preserves independent fields and their accuracy. An unavailable dependency or unsafe upper-bound substitution makes only the affected field unavailable; it never performs budget pruning or path ranking. - Concrete instance parameters come from each endpoint instance's `Problem::parameters()` implementation; `ReductionEntry` stores only the symbolic parameter relation. -- Rules producing `ILP` must declare known finite variable domains with `ILP::with_variables`; constraint rows alone do not supply bounds to binary encoding. Bounds on auxiliary variables must preserve feasibility and the optimum. Document genuinely unbounded variables rather than inventing a cutoff. +- Rules producing ILP must target the most specific applicable registered variant: `ILP` for binary variables, `ILP` for explicit finite integer domains, and general `ILP` otherwise. Supply finite domains with `ILP::with_variables`; constraint rows alone do not supply bounds to binary encoding. Bounds on auxiliary variables must preserve feasibility and the optimum. The independent `bounds` dimension defaults to `general`; register additional concrete variants only when a reduction needs them. - `VariantEntry` has both a complexity string and compiled `complexity_eval_fn` — same pattern - Expressions support: constants, variables, `+`, `-`, `*`, `/`, `^`, `exp()`, `log()`, `sqrt()`, `factorial()` - Complexity strings must use **concrete numeric values only** (e.g., `"2^(2.372 * num_vertices / 3)"`, not `"2^(omega * num_vertices / 3)"`) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 781280a3a..85c8670e9 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -16946,13 +16946,13 @@ The following table shows concrete target-variable counts for example instances, // === Non-ILP reduction rules (issue #974) === #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. + ILP variants convert between binary and bounded integer variable domains and between exact-integer and floating-point coefficients. The independent bounds dimension defaults to general; the bounded integer variant requires finite lower and upper endpoints for every variable. Binary variables embed into bounded integer ILP, which embeds into general integer ILP while retaining all stored bounds. A 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-bounded-integer and bounded-to-general edges, 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 is registered only for the bounded integer variant; its constructor requires two finite bounds for every variable. 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. - _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. + _Correctness._ The binary-to-bounded-integer and bounded-to-general embeddings change 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 changes the stored objective only by the constant lower-bound contribution, so optimal assignments are preserved. 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-bounded-integer, bounded-to-general, 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)$. ] #let hc_hp = load-example("HamiltonianCircuit", "HamiltonianPath") diff --git a/docs/src/design.md b/docs/src/design.md index ebe73dac6..e63a6ac7a 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -516,6 +516,34 @@ Big-O display; it does not rank or filter paths. ## Solvers +### ILP bounds variants + +`ILP` separates the variable domain, coefficient type, and bounds +requirement. `B` defaults to `General`: omitted bounds in a user problem +reference resolve to `bounds=general`. General ILP accepts finite and infinite +variable intervals. Binary variables still have `[0, 1]` domains independently +of this dimension. + +`ILP` requires explicit finite endpoints for every variable, +validated by both construction and deserialization. Use `with_variables` to +supply them; no bounds are inferred from constraint rows. It is the only +additional concrete registration. There is no bounded-binary or bounded-float +registration. + +Binary ILP embeds into bounded integer ILP, which embeds into general integer +ILP without removing any stored bounds or changing the objective. Binary +encoding starts only from bounded integer ILP. Incoming rules target binary ILP +when all variables are binary and bounded integer ILP when finite integer +domains are part of their construction. Their bounds must preserve source +feasibility and optima. + +These are mathematical graph edges. Arithmetic overflow remains an execution +error, and backend availability belongs to the separate solver capability +registry. Fixed solver pipelines stop directly at the registered bounded ILP +terminal; they do not need the embedding into general ILP. + +### Solver execution + The reference solver exposes a direct typed operation: ```rust,ignore @@ -528,7 +556,7 @@ 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, terminating at the native `ILP` with `bool`/`i64` variables and `i64`/`f64` coefficients. `HighsAdapter` owns numerical conversion, backend settings, termination status, and returned-assignment validation. Integer terminals go directly to the adapter; the explicit integer-to-float reduction remains available but is not part of solver pipelines. Optimality and infeasibility follow HiGHS numerical tolerances; the adapter does not provide exact proofs. | +| **ILPSolver** | Executes a problem's registered ILP pipeline, terminating at the native `ILP` with `bool`/`i64` variables and `i64`/`f64` coefficients. `HighsAdapter` owns numerical conversion, backend settings, termination status, and returned-assignment validation. Integer terminals go directly to the adapter; the explicit integer-to-float reduction remains available but is not part of solver pipelines. Optimality and infeasibility follow HiGHS numerical tolerances; the adapter does not provide exact proofs. | ILP results are optimal or infeasible according to HiGHS numerical tolerances; zero MIP gaps do not imply mathematical exactness. Integer extraction rounds diff --git a/examples/chained_reduction_factoring_to_spinglass.rs b/examples/chained_reduction_factoring_to_spinglass.rs index c07f0faa8..d3a2a6969 100644 --- a/examples/chained_reduction_factoring_to_spinglass.rs +++ b/examples/chained_reduction_factoring_to_spinglass.rs @@ -1,3 +1,4 @@ +use problemreductions::models::algebraic::Bounded; // # Chained Reduction: Factoring -> SpinGlass // // Mirrors Julia's examples/Ising.jl — reduces a Factoring problem @@ -43,9 +44,10 @@ pub fn run() -> std::result::Result<(), Box> { // ANCHOR_END: step2 // ANCHOR: step3 - // Factoring reduces to ILP, so we manually reduce, solve, and extract + // Factoring reduces to ILP, so we manually reduce, solve, and extract let solver = ILPSolver::new(); - let reduction = ReduceTo::>::reduce_to(&factoring).expect("reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(&factoring) + .expect("reduction should succeed"); let ilp_solution = solver.solve(reduction.target_problem()).unwrap(); let solution = reduction.extract_solution(&ilp_solution).unwrap(); // ANCHOR_END: step3 diff --git a/problemreductions-cli/src/create_args.rs b/problemreductions-cli/src/create_args.rs index 0b559465f..27c446fcd 100644 --- a/problemreductions-cli/src/create_args.rs +++ b/problemreductions-cli/src/create_args.rs @@ -238,7 +238,7 @@ fn invalid_problem_spec(command: &Command, message: String) -> Error { } fn canonical_problem_spec(problem: &ProblemType, variant: &BTreeMap) -> String { - let values = problem + let mut values = problem .dimensions .iter() .filter_map(|dimension| { @@ -246,6 +246,20 @@ fn canonical_problem_spec(problem: &ProblemType, variant: &BTreeMap>(); + if values.iter().any(|value| { + problem + .dimensions + .iter() + .filter(|dimension| dimension.allowed_values.contains(value)) + .count() + > 1 + }) { + values = problem + .dimensions + .iter() + .map(|dimension| dimension_value(variant, dimension.key, dimension.default_value)) + .collect(); + } join_spec(problem.canonical_name, &values) } @@ -284,6 +298,19 @@ fn add_value_parser(arg: Arg, kind: crate::commands::create::InputValueKind) -> #[cfg(test)] mod tests { + #[test] + fn canonical_create_specs_resolve_to_the_original_variant() { + let graph = problemreductions::rules::ReductionGraph::new(); + for entry in problemreductions::registry::variant_entries() { + let problem = problemreductions::registry::find_problem_type(entry.name).unwrap(); + let variant = entry.variant_map(); + let spec = super::canonical_problem_spec(&problem, &variant); + let resolved = crate::problem_name::resolve_problem_ref(&spec, &graph) + .unwrap_or_else(|error| panic!("{spec}: {error}")); + assert_eq!(resolved.variant, variant, "{spec}"); + } + } + #[test] fn decision_create_help_includes_field_descriptions_and_bound_direction() { for (spec, direction) in [("DecisionMaxCut", ">="), ("DecisionQUBO", "<=")] { diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 883ebc1ca..cebb9591b 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -853,7 +853,7 @@ mod tests { let route = crate::commands::reduce::parse_path_json( r#"{"path":[{ "from":{"name":"ExpectedRetrievalCost","variant":{}}, - "to":{"name":"ILP","variant":{"coefficient":"f64","variable":"bool"}} + "to":{"name":"ILP","variant":{"coefficient":"f64","variable":"bool","bounds":"general"}} }]}"#, ) .unwrap(); diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 1c3057466..96035a9eb 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -156,19 +156,21 @@ fn resolve_variant_updates( let problem = problemreductions::registry::find_problem_type(&spec.name) .expect("registered problem has a schema"); - if spec.variant_values.len() == problem.dimensions.len() + if spec.variant_values.len() <= problem.dimensions.len() && problem .dimensions .iter() .zip(&spec.variant_values) .all(|(dimension, value)| dimension.allowed_values.contains(&value.as_str())) { - let resolved = problem - .dimensions - .iter() - .zip(&spec.variant_values) - .map(|(dimension, value)| (dimension.key.to_string(), value.clone())) - .collect(); + let mut resolved = default_variant.clone(); + resolved.extend( + problem + .dimensions + .iter() + .zip(&spec.variant_values) + .map(|(dimension, value)| (dimension.key.to_string(), value.clone())), + ); anyhow::ensure!( known_variants.contains(&resolved), "Resolved variant {} is not declared for {}", @@ -406,10 +408,29 @@ mod tests { let resolved = resolve_problem_ref("ILP/bool/i64", &graph).unwrap(); assert_eq!(resolved.variant["variable"], "bool"); assert_eq!(resolved.variant["coefficient"], "i64"); + assert_eq!(resolved.variant["bounds"], "general"); assert_eq!( crate::commands::graph::variant_to_full_slash("ILP", &resolved.variant), - "/bool/i64" + "/bool/i64/general" + ); + } + + #[test] + fn ilp_bounds_default_to_general_without_registering_unused_combinations() { + let graph = problemreductions::rules::ReductionGraph::new(); + for spec in ["ILP/i64", "ILP/i64/i64", "ILP/variable=i64"] { + let resolved = resolve_problem_ref(spec, &graph).unwrap(); + assert_eq!(resolved.variant["variable"], "i64"); + assert_eq!(resolved.variant["bounds"], "general"); + } + assert_eq!( + resolve_problem_ref("ILP/i64/i64/bounded", &graph) + .unwrap() + .variant["bounds"], + "bounded" ); + assert!(resolve_problem_ref("ILP/i64/f64/bounded", &graph).is_err()); + assert!(resolve_problem_ref("ILP/bool/i64/bounded", &graph).is_err()); } #[test] diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index e5641a0a5..37f2ad929 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -3588,7 +3588,7 @@ fn test_solve_bundle_ilp() { } #[test] -fn test_solve_direct_ilp_i64_problem() { +fn test_solve_direct_bounded_integer_ilp_problem() { let problem_file = std::env::temp_dir().join("pred_test_solve_ilp_i64_problem.json"); let create_out = pred() @@ -3599,7 +3599,7 @@ fn test_solve_direct_ilp_i64_problem() { "--example", "SequencingToMinimizeWeightedCompletionTime", "--to", - "ILP/variable=i64", + "ILP/variable=i64/bounds=bounded", "--example-side", "target", ]) @@ -9750,13 +9750,17 @@ fn test_extract_rejects_infeasible_target_even_when_decoded_source_is_feasible() use serde_json::json; let source = OpenShopScheduling::new(1, vec![vec![1]]); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = + ReduceTo::>::reduce_to( + &source, + ) + .unwrap(); let bundle = std::env::temp_dir().join(format!( "pred-extract-target-feasibility-{}.json", std::process::id() )); let source_key = json!({"name":"OpenShopScheduling","variant":{}}); - let target_variant = json!({"variable":"i64","coefficient":"i64"}); + let target_variant = json!({"variable":"i64","coefficient":"i64","bounds":"bounded"}); std::fs::write( &bundle, json!({ diff --git a/src/example_db/specs.rs b/src/example_db/specs.rs index 75f2210f4..39d9dd8e8 100644 --- a/src/example_db/specs.rs +++ b/src/example_db/specs.rs @@ -4,6 +4,9 @@ //! that can be validated against the catalog and reduction registry. use crate::export::{ProblemSide, RuleExample, SolutionPair}; +use crate::models::algebraic::{ + Bounded, BoundsPolicy, General, ILPCoefficient, VariableDomain, ILP, +}; use crate::prelude::{Problem, ReduceTo, ReductionResult}; use crate::registry::DynProblem; use serde::Serialize; @@ -70,34 +73,43 @@ where /// the double `reduce_to()` that would occur with `rule_example_with_witness`. pub fn rule_example_via_ilp(source: S) -> RuleExample where - S: Problem + Serialize + ReduceTo>, - V: crate::models::algebraic::VariableDomain, - >>::Result: - ReductionResult>, + S: Problem + Serialize + ReduceTo>, + V: VariableDomain, + >>::Result: ReductionResult>, S::Solution: Serialize, { - rule_example_via_typed_ilp::(source) + rule_example_via_typed_ilp::(source) +} + +/// Integer ILP example with explicit finite variable domains. +pub fn rule_example_via_bounded_ilp(source: S) -> RuleExample +where + S: Problem + Serialize + ReduceTo>, + >>::Result: + ReductionResult>, + S::Solution: Serialize, +{ + rule_example_via_typed_ilp::(source) } /// Float-coefficient counterpart of [`rule_example_via_ilp`]. pub fn rule_example_via_float_ilp(source: S) -> RuleExample where - S: Problem + Serialize + ReduceTo>, - V: crate::models::algebraic::VariableDomain, - >>::Result: - ReductionResult>, + S: Problem + Serialize + ReduceTo>, + V: VariableDomain, + >>::Result: ReductionResult>, S::Solution: Serialize, { - rule_example_via_typed_ilp::(source) + rule_example_via_typed_ilp::(source) } -fn rule_example_via_typed_ilp(source: S) -> RuleExample +fn rule_example_via_typed_ilp(source: S) -> RuleExample where - S: Problem + Serialize + ReduceTo>, - V: crate::models::algebraic::VariableDomain, - C: crate::models::algebraic::ILPCoefficient + Serialize, - >>::Result: - ReductionResult>, + S: Problem + Serialize + ReduceTo>, + V: VariableDomain, + C: ILPCoefficient + Serialize, + B: BoundsPolicy, + >>::Result: ReductionResult>, S::Solution: Serialize, { use crate::export::SolutionPair; diff --git a/src/models/algebraic/ilp.rs b/src/models/algebraic/ilp.rs index 7f4111e35..3c61cfeac 100644 --- a/src/models/algebraic/ilp.rs +++ b/src/models/algebraic/ilp.rs @@ -21,6 +21,7 @@ inventory::submit! { dimensions: &[ VariantDimension::new("variable", "bool", &["bool", "i64"]), VariantDimension::new("coefficient", "i64", &["i64", "f64"]), + VariantDimension::new("bounds", "general", &["general", "bounded"]), ], category: crate::registry::ProblemCategory::Algebraic, module_path: module_path!(), @@ -46,6 +47,46 @@ pub trait VariableDomain: 'static + Clone + Debug + Send + Sync { fn validate_variables(variables: &[IntegerVariable]) -> Result<(), ConstructionError>; } +/// Type-level requirement on the explicitly stored variable intervals. +pub trait BoundsPolicy: 'static + Clone + Debug + Send + Sync { + /// Registered bounds dimension value. + const NAME: &'static str; + /// Validate the interval requirement independently of the variable domain. + fn validate_variables(variables: &[IntegerVariable]) -> Result<(), ConstructionError>; +} + +/// Variable intervals may have infinite endpoints. +#[derive(Debug, Clone, Copy)] +pub struct General; + +impl BoundsPolicy for General { + const NAME: &'static str = "general"; + + fn validate_variables(_variables: &[IntegerVariable]) -> Result<(), ConstructionError> { + Ok(()) + } +} + +/// Every variable has explicit finite lower and upper bounds. +#[derive(Debug, Clone, Copy)] +pub struct Bounded; + +impl BoundsPolicy for Bounded { + const NAME: &'static str = "bounded"; + + fn validate_variables(variables: &[IntegerVariable]) -> Result<(), ConstructionError> { + if variables + .iter() + .any(|variable| variable.lower_bound().is_none() || variable.upper_bound().is_none()) + { + return Err(ConstructionError::Conversion( + "bounded ILP requires finite lower and upper bounds for every variable".into(), + )); + } + Ok(()) + } +} + /// Numeric domain shared by an ILP's constraints, right-hand sides, and objective. pub trait ILPCoefficient: NumericSize + WeightElement + Copy + Debug + Send + Sync @@ -318,13 +359,13 @@ pub enum ObjectiveSense { /// Integer Linear Programming model. #[derive(Debug, Clone, Serialize)] -pub struct ILP { +pub struct ILP { variables: Vec, constraints: Vec>, objective: Vec<(usize, C)>, sense: ObjectiveSense, #[serde(skip)] - marker: PhantomData, + marker: PhantomData<(V, B)>, } #[derive(Deserialize)] @@ -335,10 +376,11 @@ struct ILPData { sense: ObjectiveSense, } -impl<'de, V, C> Deserialize<'de> for ILP +impl<'de, V, C, B> Deserialize<'de> for ILP where V: VariableDomain, C: ILPCoefficient + Deserialize<'de>, + B: BoundsPolicy, { fn deserialize(deserializer: D) -> Result where @@ -350,9 +392,10 @@ where } } -impl ILP { +impl ILP { /// Construct a homogeneous model using the domain certificate's standard /// variable interval: binary `[0, 1]` or integer `[0, +∞)`. + /// Bounded integer models with variables must use [`Self::with_variables`]. pub fn new( num_variables: usize, constraints: Vec>, @@ -375,6 +418,7 @@ impl ILP { sense: ObjectiveSense, ) -> Result { V::validate_variables(&variables)?; + B::validate_variables(&variables)?; let num_variables = variables.len(); let constraints = constraints .into_iter() @@ -578,7 +622,7 @@ fn normalize_objective( Ok(normalized) } -impl Problem for ILP { +impl Problem for ILP { const NAME: &'static str = "ILP"; type Solution = Vec; type Value = Extremum; @@ -604,39 +648,60 @@ impl Problem for ILP { } fn variant() -> Vec<(&'static str, &'static str)> { - vec![("variable", V::NAME), ("coefficient", C::NAME)] + vec![ + ("variable", V::NAME), + ("coefficient", C::NAME), + ("bounds", B::NAME), + ] } } crate::declare_variants! { default ILP => "2^num_vars", ILP => "num_vars^num_vars", + ILP => "num_vars^num_vars", ILP => "2^num_vars", ILP => "num_vars^num_vars", } #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { - vec![crate::example_db::specs::ModelExampleSpec { - id: "ilp", - instance: Box::new( - ILP::::new( - 2, - vec![ - LinearConstraint::le(vec![(0, 1), (1, 1)], 5), - LinearConstraint::le(vec![(0, 4), (1, 7)], 28), - ], - vec![(0, -5), (1, -6)], - ObjectiveSense::Minimize, - ) - .expect("canonical ILP construction must succeed"), - ), - optimal_config: serde_json::json!(vec![3, 2]), - optimal_value: serde_json::json!({ - "sense": "Minimize", - "value": -27, - }), - }] + vec![ + crate::example_db::specs::ModelExampleSpec { + id: "ilp", + instance: Box::new( + ILP::::new( + 2, + vec![ + LinearConstraint::le(vec![(0, 1), (1, 1)], 5), + LinearConstraint::le(vec![(0, 4), (1, 7)], 28), + ], + vec![(0, -5), (1, -6)], + ObjectiveSense::Minimize, + ) + .expect("canonical ILP construction must succeed"), + ), + optimal_config: serde_json::json!(vec![3, 2]), + optimal_value: serde_json::json!({ + "sense": "Minimize", + "value": -27, + }), + }, + crate::example_db::specs::ModelExampleSpec { + id: "bounded_ilp", + instance: Box::new( + ILP::::with_variables( + vec![IntegerVariable::new(Some(-2), Some(3)).unwrap()], + vec![LinearConstraint::le(vec![(0, 1)], 1)], + vec![(0, 3)], + ObjectiveSense::Maximize, + ) + .unwrap(), + ), + optimal_config: serde_json::json!([1]), + optimal_value: serde_json::json!({"sense": "Maximize", "value": 3}), + }, + ] } #[cfg(test)] diff --git a/src/models/algebraic/mod.rs b/src/models/algebraic/mod.rs index 9568a0a8e..c4b7393c9 100644 --- a/src/models/algebraic/mod.rs +++ b/src/models/algebraic/mod.rs @@ -47,8 +47,8 @@ pub use consecutive_ones_submatrix::ConsecutiveOnesSubmatrix; pub use equilibrium_point::EquilibriumPoint; pub use feasible_basis_extension::FeasibleBasisExtension; pub use ilp::{ - Comparison, ILPCoefficient, IntegerVariable, LinearConstraint, ObjectiveSense, VariableDomain, - ILP, + Bounded, BoundsPolicy, Comparison, General, ILPCoefficient, IntegerVariable, LinearConstraint, + ObjectiveSense, VariableDomain, ILP, }; pub use minimum_matrix_cover::MinimumMatrixCover; pub use minimum_matrix_domination::MinimumMatrixDomination; diff --git a/src/rules/acyclicpartition_ilp.rs b/src/rules/acyclicpartition_ilp.rs index f042f7b37..27d5c54a0 100644 --- a/src/rules/acyclicpartition_ilp.rs +++ b/src/rules/acyclicpartition_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from AcyclicPartition to `ILP`. +//! Reduction from AcyclicPartition to `ILP`. //! //! One-hot assignment x_{v,c}, McCormick same-class indicators s_{t,c}, //! crossing flags y_t, and partition labels used directly as a topological order. @@ -12,15 +12,15 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionAcyclicPartitionToILP { - target: ILP, + target: ILP, n: usize, } impl ReductionResult for ReductionAcyclicPartitionToILP { type Source = AcyclicPartition; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -52,7 +52,7 @@ impl crate::rules::AggregateReductionResult for ReductionAcyclicPartitionToILP { num_nonzeros = "(num_vertices * num_vertices + num_arcs * num_vertices + num_arcs + num_vertices) * (num_vertices^2 + 4 * num_vertices + 3 * num_arcs * num_vertices + 2 * num_arcs + 1)", }, })] -impl ReduceTo> for AcyclicPartition { +impl ReduceTo> for AcyclicPartition { type Result = ReductionAcyclicPartitionToILP; fn reduce_to(&self) -> Result { @@ -101,7 +101,7 @@ impl ReduceTo> for AcyclicPartition { terms.push(( used_idx(c), weight_bound.checked_neg().ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::>( + crate::rules::ReductionError::integer_overflow::>( "negating the partition weight bound", ) })?, @@ -170,13 +170,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source) + crate::rules::ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let ilp_sol = crate::solvers::ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_sol).unwrap(); - crate::example_db::specs::rule_example_with_witness::<_, ILP>( + crate::example_db::specs::rule_example_with_witness::<_, ILP>( source, SolutionPair { source_config: serde_json::json!(extracted), diff --git a/src/rules/biconnectivityaugmentation_ilp.rs b/src/rules/biconnectivityaugmentation_ilp.rs index af0513832..287d8a603 100644 --- a/src/rules/biconnectivityaugmentation_ilp.rs +++ b/src/rules/biconnectivityaugmentation_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from BiconnectivityAugmentation to `ILP`. +//! Reduction from BiconnectivityAugmentation to `ILP`. //! //! Select candidate edges under budget and, both before deletion and for every deleted vertex q, //! certify that the remaining augmented graph stays connected via unit-flow @@ -12,7 +12,7 @@ use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] pub struct ReductionBiconnAugToILP { - target: ILP, + target: ILP, num_candidates: usize, } @@ -25,7 +25,7 @@ impl ReductionBiconnAugToILP { let overflow = || { crate::rules::ReductionError::integer_overflow::< BiconnectivityAugmentation, - ILP, + ILP, >("computing connectivity flow variable counts") }; let commodities = n @@ -48,9 +48,9 @@ impl ReductionBiconnAugToILP { impl ReductionResult for ReductionBiconnAugToILP { type Source = BiconnectivityAugmentation; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -80,7 +80,7 @@ impl crate::rules::AggregateReductionResult for ReductionBiconnAugToILP {} num_constraints = "1 + num_vertices * (num_vertices + 1) * (2 * num_edges + 4 * num_potential_edges + num_vertices)", num_nonzeros = "(num_potential_edges + 2 * num_vertices * (num_vertices + 1) * (num_edges + num_potential_edges)) * (1 + num_vertices * (num_vertices + 1) * (2 * num_edges + 4 * num_potential_edges + num_vertices))", })] -impl ReduceTo> for BiconnectivityAugmentation { +impl ReduceTo> for BiconnectivityAugmentation { type Result = ReductionBiconnAugToILP; fn reduce_to(&self) -> Result { @@ -242,13 +242,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source) + crate::rules::ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let ilp_sol = crate::solvers::ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_sol).unwrap(); - crate::example_db::specs::rule_example_with_witness::<_, ILP>( + crate::example_db::specs::rule_example_with_witness::<_, ILP>( source, SolutionPair { source_config: serde_json::json!(extracted), diff --git a/src/rules/bottlenecktravelingsalesman_ilp.rs b/src/rules/bottlenecktravelingsalesman_ilp.rs index 5c36ad7d8..48925ed95 100644 --- a/src/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/rules/bottlenecktravelingsalesman_ilp.rs @@ -10,16 +10,16 @@ use crate::topology::Graph; /// One selected maximum-weight edge carries the exact objective coefficient. #[derive(Debug, Clone)] pub struct ReductionBTSPToILP { - target: ILP, + target: ILP, num_vertices: usize, num_edges: usize, } impl ReductionResult for ReductionBTSPToILP { type Source = BottleneckTravelingSalesman; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -48,7 +48,7 @@ impl ReductionBTSPToILP { m: usize, ) -> Result<(usize, usize, usize, usize), crate::rules::ReductionError> { let overflow = || { - crate::rules::ReductionError::integer_overflow::>( + crate::rules::ReductionError::integer_overflow::>( "sizing the cyclic edge-selection formulation", ) }; @@ -71,7 +71,7 @@ impl ReductionBTSPToILP { }) .and_then(|v| v.checked_add(1)) .ok_or_else(overflow)?; - >>::exact_i64( + >>::exact_i64( vars, "bounding binary constraint accumulation", )?; @@ -88,7 +88,7 @@ impl ReductionBTSPToILP { num_nonzeros = "(num_vertices^2 + 2 * num_edges * num_vertices + num_edges) * (num_vertices^2 + 6 * num_edges * num_vertices + 4 * num_edges + 3 * num_vertices + 1)", }, })] -impl ReduceTo> for BottleneckTravelingSalesman { +impl ReduceTo> for BottleneckTravelingSalesman { type Result = ReductionBTSPToILP; fn reduce_to(&self) -> Result { @@ -108,7 +108,7 @@ impl ReduceTo> for BottleneckTravelingSalesman { .collect::>() }; let mut constraints = Vec::with_capacity(num_constraints); - // ILP variables are nonnegative. Check binary bounds before sums. + // ILP variables are nonnegative. Check binary bounds before sums. for variable in 0..num_vars { constraints.push(LinearConstraint::le(vec![(variable, 1)], 1)); } @@ -197,7 +197,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_ilp::<_, bool>(source) }, }] } diff --git a/src/rules/boundedcomponentspanningforest_ilp.rs b/src/rules/boundedcomponentspanningforest_ilp.rs index 093f35112..d64e72aac 100644 --- a/src/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/rules/boundedcomponentspanningforest_ilp.rs @@ -1,10 +1,10 @@ -//! Reduction from BoundedComponentSpanningForest to `ILP`. +//! Reduction from BoundedComponentSpanningForest to `ILP`. //! //! Assign every vertex to one of K components, bound weight, certify //! connectivity inside each used component via flow. //! See the paper entry for the full formulation. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BoundedComponentSpanningForest; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; @@ -13,16 +13,16 @@ use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] pub struct ReductionBCSFToILP { - target: ILP, + target: ILP, n: usize, k: usize, } impl ReductionResult for ReductionBCSFToILP { type Source = BoundedComponentSpanningForest; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -54,7 +54,7 @@ impl crate::rules::AggregateReductionResult for ReductionBCSFToILP {} num_nonzeros = "(3 * num_vertices * max_components + 2 * max_components + 2 * num_edges * max_components) * (num_vertices + 5 * max_components + 6 * num_vertices * max_components + 6 * num_edges * max_components)", }, })] -impl ReduceTo> for BoundedComponentSpanningForest { +impl ReduceTo> for BoundedComponentSpanningForest { type Result = ReductionBCSFToILP; fn reduce_to(&self) -> Result { @@ -214,13 +214,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source) + crate::rules::ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let ilp_sol = crate::solvers::ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_sol).unwrap(); - crate::example_db::specs::rule_example_with_witness::<_, ILP>( + crate::example_db::specs::rule_example_with_witness::<_, ILP>( source, SolutionPair { source_config: serde_json::json!(extracted), diff --git a/src/rules/closeststring_ilp.rs b/src/rules/closeststring_ilp.rs index 852f45288..7d2ecbb6c 100644 --- a/src/rules/closeststring_ilp.rs +++ b/src/rules/closeststring_ilp.rs @@ -21,29 +21,29 @@ //! substring problems," Journal of the ACM 49(2):157-171, 2002. //! -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ClosestString; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing ClosestString to ILP. /// -/// Variable layout (`ILP`, all non-negative): +/// Variable layout (`ILP`, all non-negative): /// - `x_{j, a}` at index `j * alphabet_size + a` for `j in [0, m)` and /// `a in [0, q)`, bounded to `{0, 1}`. /// - `R` (radius) at index `m * q`, an integer in `[0, m]`. #[derive(Debug, Clone)] pub struct ReductionClosestStringToILP { - target: ILP, + target: ILP, alphabet_size: usize, string_length: usize, } impl ReductionResult for ReductionClosestStringToILP { type Source = ClosestString; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -85,7 +85,7 @@ impl ReductionResult for ReductionClosestStringToILP { num_nonzeros = "alphabet_size * string_length + num_strings * (string_length + 1)", } )] -impl ReduceTo> for ClosestString { +impl ReduceTo> for ClosestString { type Result = ReductionClosestStringToILP; fn reduce_to(&self) -> Result { @@ -101,7 +101,7 @@ impl ReduceTo> for ClosestString { let mut constraints: Vec = Vec::with_capacity(m + n); // Assignment constraints: exactly one symbol per center position. - // Together with the non-negativity built into `ILP`, this also + // Together with the non-negativity built into `ILP`, this also // forces every x_{j, a} to lie in {0, 1}. for j in 0..m { let terms: Vec<(usize, i64)> = (0..q).map(|a| (x_idx(j, a), 1)).collect(); @@ -157,7 +157,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/closestsubstring_ilp.rs b/src/rules/closestsubstring_ilp.rs index 8bb7589b9..8328ece0a 100644 --- a/src/rules/closestsubstring_ilp.rs +++ b/src/rules/closestsubstring_ilp.rs @@ -29,14 +29,14 @@ //! substring problems," Journal of the ACM 49(2):157-171, 2002. //! -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ClosestSubstring; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing ClosestSubstring to ILP. /// -/// Variable layout (`ILP`, all non-negative): +/// Variable layout (`ILP`, all non-negative): /// - `x_{r, a}` at index `r * alphabet_size + a` for `r in [0, ell)` and /// `a in [0, q)`, forced into `{0, 1}` by the assignment constraints. /// - `y_{i, p}` at index `q * ell + window_offsets[i] + p` for input string @@ -46,7 +46,7 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// integer in `[0, ell]`. #[derive(Debug, Clone)] pub struct ReductionClosestSubstringToILP { - target: ILP, + target: ILP, alphabet_size: usize, substring_length: usize, /// Prefix sums of per-string window counts: `window_offsets[i]` is the @@ -58,9 +58,9 @@ pub struct ReductionClosestSubstringToILP { impl ReductionResult for ReductionClosestSubstringToILP { type Source = ClosestSubstring; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -127,7 +127,7 @@ fn decode_one_hot( num_nonzeros = "(alphabet_size * substring_length + total_num_windows + 1) * (substring_length + num_strings + total_num_windows + 1)", }, })] -impl ReduceTo> for ClosestSubstring { +impl ReduceTo> for ClosestSubstring { type Result = ReductionClosestSubstringToILP; fn reduce_to(&self) -> Result { @@ -158,7 +158,7 @@ impl ReduceTo> for ClosestSubstring { Vec::with_capacity(ell + n + total_windows + 1); // Assignment constraints: exactly one symbol per center position. - // Together with the non-negativity built into `ILP`, this also + // Together with the non-negativity built into `ILP`, this also // forces every x_{r, a} to lie in {0, 1}. for r in 0..ell { let terms: Vec<(usize, i64)> = (0..q).map(|a| (x_idx(r, a), 1)).collect(); @@ -237,7 +237,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/directedtwocommodityintegralflow_ilp.rs b/src/rules/directedtwocommodityintegralflow_ilp.rs index de6b9e0ef..1c2028996 100644 --- a/src/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/rules/directedtwocommodityintegralflow_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from DirectedTwoCommodityIntegralFlow to `ILP`. +//! Reduction from DirectedTwoCommodityIntegralFlow to `ILP`. //! //! One non-negative integer variable per (commodity, arc): //! f1_a = a for a in 0..num_arcs (commodity 1 flow on arc a) @@ -12,27 +12,27 @@ //! Objective: Minimize 0 (feasibility). //! Extraction: Direct 2*|A| variables. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::DirectedTwoCommodityIntegralFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing DirectedTwoCommodityIntegralFlow to `ILP`. +/// Result of reducing DirectedTwoCommodityIntegralFlow to `ILP`. /// /// Variable layout: /// - `f1_a` at index a for a in 0..num_arcs (commodity 1) /// - `f2_a` at index num_arcs + a for a in 0..num_arcs (commodity 2) #[derive(Debug, Clone)] pub struct ReductionD2CIFToILP { - target: ILP, + target: ILP, num_arcs: usize, } impl ReductionResult for ReductionD2CIFToILP { type Source = DirectedTwoCommodityIntegralFlow; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -60,7 +60,7 @@ impl crate::rules::AggregateReductionResult for ReductionD2CIFToILP {} num_constraints = "num_arcs + 2 * num_vertices + 2", num_nonzeros = "(2 * num_arcs) * (num_arcs + 2 * num_vertices + 2)", })] -impl ReduceTo> for DirectedTwoCommodityIntegralFlow { +impl ReduceTo> for DirectedTwoCommodityIntegralFlow { type Result = ReductionD2CIFToILP; fn reduce_to(&self) -> Result { @@ -201,7 +201,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/ensemblecomputation_ilp.rs b/src/rules/ensemblecomputation_ilp.rs index edd086456..520017694 100644 --- a/src/rules/ensemblecomputation_ilp.rs +++ b/src/rules/ensemblecomputation_ilp.rs @@ -1,4 +1,4 @@ -//! Polynomial-size circuit-slot reduction from EnsembleComputation to `ILP`. +//! Polynomial-size circuit-slot reduction from EnsembleComputation to `ILP`. use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::EnsembleComputation; @@ -7,7 +7,7 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionEnsembleComputationToILP { - target: ILP, + target: ILP, universe_size: usize, budget: usize, activity_base: usize, @@ -32,7 +32,7 @@ impl ReductionEnsembleComputationToILP { impl ReductionResult for ReductionEnsembleComputationToILP { type Source = EnsembleComputation; - type Target = ILP; + type Target = ILP; fn target_problem(&self) -> &Self::Target { &self.target @@ -89,7 +89,7 @@ impl ReductionResult for ReductionEnsembleComputationToILP { num_nonzeros = "(3 * budget * universe_size + budget * (budget - 1) * (universe_size + 1) + num_subsets * budget + budget) * (5 * budget - 1 + budget * (budget - 1) * (1 + 3 * universe_size) + 2 * budget * universe_size + num_subsets * budget * (universe_size + 2) + num_subsets)", }, })] -impl ReduceTo> for EnsembleComputation { +impl ReduceTo> for EnsembleComputation { type Result = ReductionEnsembleComputationToILP; fn reduce_to(&self) -> Result { @@ -97,7 +97,7 @@ impl ReduceTo> for EnsembleComputation { let budget = self.budget(); let t = self.num_subsets(); let overflow = |operation| { - crate::rules::ReductionError::integer_overflow::>( + crate::rules::ReductionError::integer_overflow::>( operation, ) }; @@ -301,7 +301,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_ilp::<_, bool>(source) }, }] } diff --git a/src/rules/eulerianpath_ilp.rs b/src/rules/eulerianpath_ilp.rs index 64741308a..eb8374704 100644 --- a/src/rules/eulerianpath_ilp.rs +++ b/src/rules/eulerianpath_ilp.rs @@ -23,12 +23,12 @@ //! Bang-Jensen and Gutin, *Digraphs: Theory, Algorithms and Applications*, //! 2nd ed., Springer (2009). -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::EulerianPath; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing EulerianPath to `ILP`. +/// Result of reducing EulerianPath to `ILP`. /// /// Variable layout (all in the non-negative integer domain, with explicit /// upper bounds enforcing the intended `0/1` and `0..m-1` ranges): @@ -42,7 +42,7 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// where `p = pairs.len()` is the number of compatible ordered pairs. #[derive(Debug, Clone)] pub struct ReductionEulerianPathToILP { - target: ILP, + target: ILP, /// Compatible ordered pairs `(a, b)` in the order their `y_{a,b}` variables /// appear in the ILP, for `m > 0`. Empty when `m = 0`. pairs: Vec<(usize, usize)>, @@ -58,9 +58,9 @@ impl ReductionEulerianPathToILP { impl ReductionResult for ReductionEulerianPathToILP { type Source = EulerianPath; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -152,7 +152,7 @@ impl crate::rules::AggregateReductionResult for ReductionEulerianPathToILP {} num_constraints = "5 * num_arcs + 2 * num_arcs * num_arcs + 2", num_nonzeros = "(3 * num_arcs + num_arcs * num_arcs) * (5 * num_arcs + 2 * num_arcs * num_arcs + 2)", })] -impl ReduceTo> for EulerianPath { +impl ReduceTo> for EulerianPath { type Result = ReductionEulerianPathToILP; fn reduce_to(&self) -> Result { @@ -263,7 +263,7 @@ 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)])); - crate::example_db::specs::rule_example_via_ilp::<_, i64>(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/factoring_ilp.rs b/src/rules/factoring_ilp.rs index a9ee5b30f..c1d6f9316 100644 --- a/src/rules/factoring_ilp.rs +++ b/src/rules/factoring_ilp.rs @@ -19,7 +19,7 @@ //! 4. Binary bounds: p_i ≤ 1, q_j ≤ 1 //! 5. Carry bounds: 0 ≤ c_k ≤ min(m, n) -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::Factoring; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; @@ -34,7 +34,7 @@ use std::cmp::min; /// - Constraints enforce the multiplication equals the target #[derive(Debug, Clone)] pub struct ReductionFactoringToILP { - target: ILP, + target: ILP, m: usize, // bits for first factor n: usize, // bits for second factor } @@ -65,9 +65,9 @@ impl ReductionFactoringToILP { impl ReductionResult for ReductionFactoringToILP { type Source = Factoring; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -118,7 +118,7 @@ impl crate::rules::AggregateReductionResult for ReductionFactoringToILP {} num_constraints = "3 * num_bits_first * num_bits_second + 4 * num_bits_first + 4 * num_bits_second + 3 * target_bits + 1", num_nonzeros = "(num_bits_first * num_bits_second + 2 * num_bits_first + 2 * num_bits_second + target_bits) * (3 * num_bits_first * num_bits_second + 4 * num_bits_first + 4 * num_bits_second + 3 * target_bits + 1)", })] -impl ReduceTo> for Factoring { +impl ReduceTo> for Factoring { type Result = ReductionFactoringToILP; fn reduce_to(&self) -> Result { @@ -208,8 +208,10 @@ impl ReduceTo> for Factoring { } // Constraint 5: Carry bounds (0 ≤ c_k ≤ min(m, n)) - let carry_upper = - >>::exact_i64(min(m, n), "encoding a carry bound")?; + let carry_upper = >>::exact_i64( + min(m, n), + "encoding a carry bound", + )?; for k in 0..num_carries { let cv = carry_var(k); constraints.push(LinearConstraint::ge(vec![(cv, 1)], 0)); @@ -222,12 +224,16 @@ impl ReduceTo> for Factoring { let mut variables = vec![IntegerVariable::binary(); num_vars]; variables[num_p + num_q + num_z..].fill( IntegerVariable::new(Some(0), Some(carry_upper)) - .map_err(>>::target_construction)?, + .map_err(>>::target_construction)?, ); - let ilp = - ILP::::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) - .map_err(>>::target_construction)?; + let ilp = ILP::::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(>>::target_construction)?; Ok(ReductionFactoringToILP { target: ilp, m, n }) } @@ -239,7 +245,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/feasibleregisterassignment_ilp.rs b/src/rules/feasibleregisterassignment_ilp.rs index d56068386..bc2482e47 100644 --- a/src/rules/feasibleregisterassignment_ilp.rs +++ b/src/rules/feasibleregisterassignment_ilp.rs @@ -10,22 +10,22 @@ //! interval non-overlap: if `u` is before `v`, then `v` must be scheduled no //! earlier than the latest dependent of `u`. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::FeasibleRegisterAssignment; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionFeasibleRegisterAssignmentToILP { - target: ILP, + target: ILP, num_vertices: usize, } impl ReductionResult for ReductionFeasibleRegisterAssignmentToILP { type Source = FeasibleRegisterAssignment; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -54,7 +54,7 @@ impl crate::rules::AggregateReductionResult for ReductionFeasibleRegisterAssignm num_nonzeros = "4 * num_vertices + 4 * num_arcs + 7 * num_vertices * (num_vertices - 1) / 2 + 6 * num_same_register_pairs", } )] -impl ReduceTo> for FeasibleRegisterAssignment { +impl ReduceTo> for FeasibleRegisterAssignment { type Result = ReductionFeasibleRegisterAssignmentToILP; fn reduce_to(&self) -> Result { @@ -160,7 +160,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/flowshopscheduling_ilp.rs b/src/rules/flowshopscheduling_ilp.rs index fd91dcb2d..7bffe8cea 100644 --- a/src/rules/flowshopscheduling_ilp.rs +++ b/src/rules/flowshopscheduling_ilp.rs @@ -1,16 +1,16 @@ -//! Reduction from FlowShopScheduling to `ILP`. +//! Reduction from FlowShopScheduling to `ILP`. //! //! Binary order variables y_{i,j} with y_{i,j}=1 iff job i precedes job j, //! integer completion-time variables C_{j,q} for each job j and machine q. //! Machine-chain and big-M disjunctive constraints enforce a valid flow-shop //! schedule; the deadline becomes a makespan bound. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::FlowShopScheduling; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing FlowShopScheduling to `ILP`. +/// Result of reducing FlowShopScheduling to `ILP`. /// /// Variable layout: /// - `y_{i,j}` for each ordered pair (i,j) with i, + target: ILP, num_jobs: usize, num_machines: usize, num_order_vars: usize, @@ -28,9 +28,9 @@ pub struct ReductionFSSToILP { impl ReductionResult for ReductionFSSToILP { type Source = FlowShopScheduling; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -76,7 +76,7 @@ impl crate::rules::AggregateReductionResult for ReductionFSSToILP {} num_constraints = "num_jobs * (num_jobs - 1) + num_jobs + num_jobs * (num_processors - 1) + num_jobs * (num_jobs - 1) * num_processors + num_jobs", num_nonzeros = "(num_jobs * (num_jobs - 1) / 2 + num_jobs * num_processors) * (num_jobs * (num_jobs - 1) + num_jobs + num_jobs * (num_processors - 1) + num_jobs * (num_jobs - 1) * num_processors + num_jobs)", })] -impl ReduceTo> for FlowShopScheduling { +impl ReduceTo> for FlowShopScheduling { type Result = ReductionFSSToILP; fn reduce_to(&self) -> Result { @@ -107,9 +107,10 @@ impl ReduceTo> for FlowShopScheduling { .max() .unwrap_or(0); let big_m = d.checked_add(max_p).ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::>( - "computing the flow-shop big-M bound", - ) + crate::rules::ReductionError::integer_overflow::< + FlowShopScheduling, + ILP, + >("computing the flow-shop big-M bound") })?; let mut constraints = Vec::new(); @@ -209,7 +210,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/ilp_bool_ilp_i64.rs b/src/rules/ilp_bool_ilp_i64.rs index c5bb1df86..e62303aeb 100644 --- a/src/rules/ilp_bool_ilp_i64.rs +++ b/src/rules/ilp_bool_ilp_i64.rs @@ -1,23 +1,24 @@ -//! Natural embedding of binary ILP into general integer ILP. +//! Natural embedding of binary ILP into bounded integer ILP. //! //! The stored `[0, 1]` bounds, constraints, and objective carry over unchanged. //! //! This same-name variant reduction preserves the witness representation. +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionBinaryILPToIntILP { - target: ILP, + target: ILP, } impl ReductionResult for ReductionBinaryILPToIntILP { type Source = ILP; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -37,18 +38,18 @@ impl ReductionResult for ReductionBinaryILPToIntILP { num_constraints = "num_constraints", num_nonzeros = "num_nonzeros", },)] -impl ReduceTo> for ILP { +impl ReduceTo> for ILP { type Result = ReductionBinaryILPToIntILP; fn reduce_to(&self) -> Result { Ok(ReductionBinaryILPToIntILP { - target: ILP::::with_variables( + target: ILP::::with_variables( self.variables().to_vec(), self.constraints().to_vec(), self.objective().to_vec(), self.sense(), ) - .map_err(>>::target_construction)?, + .map_err(>>::target_construction)?, }) } } diff --git a/src/rules/ilp_bounded_ilp.rs b/src/rules/ilp_bounded_ilp.rs new file mode 100644 index 000000000..5c4679171 --- /dev/null +++ b/src/rules/ilp_bounded_ilp.rs @@ -0,0 +1,58 @@ +//! Forget the bounded-interval certificate while retaining the entire ILP. + +use crate::models::algebraic::{Bounded, ILP}; +use crate::reduction; +use crate::rules::{ReduceTo, ReductionResult}; + +/// Identity embedding of bounded integer ILP into general integer ILP. +#[derive(Debug, Clone)] +pub struct ReductionBoundedILPToILP { + target: ILP, +} + +impl ReductionResult for ReductionBoundedILPToILP { + type Source = ILP; + type Target = ILP; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_solution(&self, solution: &Vec) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_witness( + ReductionResult::target_problem(self), + solution, + |value| value.value.is_some(), + "target ILP assignment is infeasible", + )?; + Ok(solution.clone()) + } +} + +#[crate::aggregate_reduction(identity)] +impl crate::rules::AggregateReductionResult for ReductionBoundedILPToILP {} + +#[reduction(transform = exact { + num_vars = "num_vars", + num_constraints = "num_constraints", + num_nonzeros = "num_nonzeros", +})] +impl ReduceTo> for ILP { + type Result = ReductionBoundedILPToILP; + + fn reduce_to(&self) -> Result { + Ok(ReductionBoundedILPToILP { + target: ILP::with_variables( + self.variables().to_vec(), + self.constraints().to_vec(), + self.objective().to_vec(), + self.sense(), + ) + .map_err(>>::target_construction)?, + }) + } +} + +#[cfg(test)] +#[path = "../unit_tests/rules/ilp_bounded_ilp.rs"] +mod tests; diff --git a/src/rules/ilp_i64_ilp_bool.rs b/src/rules/ilp_i64_ilp_bool.rs index 4a6944ea2..055a0f450 100644 --- a/src/rules/ilp_i64_ilp_bool.rs +++ b/src/rules/ilp_i64_ilp_bool.rs @@ -1,6 +1,6 @@ //! Encode finitely bounded integer ILP variables as binary variables. -use crate::models::algebraic::{Comparison, LinearConstraint, ILP}; +use crate::models::algebraic::{Bounded, Comparison, LinearConstraint, ILP}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::rules::ReductionError; @@ -13,7 +13,7 @@ struct VarEncoding { } fn overflow(operation: impl Into) -> ReductionError { - ReductionError::integer_overflow::, ILP>(operation) + ReductionError::integer_overflow::, ILP>(operation) } fn binary_weights(width: i64) -> Vec { @@ -73,7 +73,7 @@ pub struct ReductionIntILPToBinaryILP { } impl ReductionResult for ReductionIntILPToBinaryILP { - type Source = ILP; + type Source = ILP; type Target = ILP; fn target_problem(&self) -> &ILP { @@ -119,23 +119,15 @@ impl ReductionResult for ReductionIntILPToBinaryILP { num_nonzeros = "binary expansion depends on concrete variable bounds and row sparsity", }, )] -impl ReduceTo> for ILP { +impl ReduceTo> for ILP { type Result = ReductionIntILPToBinaryILP; fn reduce_to(&self) -> Result { let mut encodings = Vec::with_capacity(self.num_vars()); let mut num_binary_variables = 0_usize; for variable in self.variables() { - let lower_bound = variable.lower_bound().ok_or_else(|| { - ReductionError::invalid_target::, ILP>( - "binary encoding requires a finite lower bound for every integer variable", - ) - })?; - let upper_bound = variable.upper_bound().ok_or_else(|| { - ReductionError::invalid_target::, ILP>( - "binary encoding requires a finite upper bound for every integer variable", - ) - })?; + let lower_bound = variable.lower_bound().expect("bounded ILP lower bound"); + let upper_bound = variable.upper_bound().expect("bounded ILP upper bound"); let width = upper_bound .checked_sub(lower_bound) .ok_or_else(|| overflow("computing an integer variable interval width"))?; diff --git a/src/rules/integerknapsack_ilp.rs b/src/rules/integerknapsack_ilp.rs index 42f56afa1..b22c6ec50 100644 --- a/src/rules/integerknapsack_ilp.rs +++ b/src/rules/integerknapsack_ilp.rs @@ -1,24 +1,24 @@ -//! Reduction from IntegerKnapsack to `ILP`. +//! Reduction from IntegerKnapsack to `ILP`. //! //! Each item multiplicity becomes a non-negative integer ILP variable. The //! capacity inequality is kept directly, and explicit upper bounds //! `c_i <= floor(B / s_i)` preserve the exact witness domain of the source. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::IntegerKnapsack; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionIntegerKnapsackToILP { - target: ILP, + target: ILP, } impl ReductionResult for ReductionIntegerKnapsackToILP { type Source = IntegerKnapsack; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -39,7 +39,7 @@ impl ReductionResult for ReductionIntegerKnapsackToILP { num_nonzeros = "2 * num_items", } )] -impl ReduceTo> for IntegerKnapsack { +impl ReduceTo> for IntegerKnapsack { type Result = ReductionIntegerKnapsackToILP; fn reduce_to(&self) -> Result { @@ -89,7 +89,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/integralflowbundles_ilp.rs b/src/rules/integralflowbundles_ilp.rs index 98038f27c..9bac3603b 100644 --- a/src/rules/integralflowbundles_ilp.rs +++ b/src/rules/integralflowbundles_ilp.rs @@ -4,7 +4,7 @@ //! the bundle-capacity inequalities, flow-conservation equalities at //! nonterminals, and the sink inflow lower bound from the source problem. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowBundles; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -12,14 +12,14 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing IntegralFlowBundles to ILP. #[derive(Debug, Clone)] pub struct ReductionIFBToILP { - target: ILP, + target: ILP, } impl ReductionResult for ReductionIFBToILP { type Source = IntegralFlowBundles; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -50,7 +50,7 @@ impl crate::rules::AggregateReductionResult for ReductionIFBToILP {} num_nonzeros = "num_arcs * (num_bundles + num_vertices - 1)", }, })] -impl ReduceTo> for IntegralFlowBundles { +impl ReduceTo> for IntegralFlowBundles { type Result = ReductionIFBToILP; fn reduce_to(&self) -> Result { @@ -119,7 +119,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/integralflowhomologousarcs_ilp.rs b/src/rules/integralflowhomologousarcs_ilp.rs index 2ec627726..7b9db705f 100644 --- a/src/rules/integralflowhomologousarcs_ilp.rs +++ b/src/rules/integralflowhomologousarcs_ilp.rs @@ -3,7 +3,7 @@ //! One integer flow variable per arc. Capacity bounds, conservation at //! non-terminals, homologous-pair equality, and sink inflow requirement. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowHomologousArcs; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -11,14 +11,14 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing IntegralFlowHomologousArcs to ILP. #[derive(Debug, Clone)] pub struct ReductionIFHAToILP { - target: ILP, + target: ILP, } impl ReductionResult for ReductionIFHAToILP { type Source = IntegralFlowHomologousArcs; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -45,7 +45,7 @@ impl crate::rules::AggregateReductionResult for ReductionIFHAToILP {} num_constraints = "num_arcs^2 + num_arcs + num_vertices + 1", num_nonzeros = "num_arcs * (num_arcs^2 + num_arcs + num_vertices + 1)", })] -impl ReduceTo> for IntegralFlowHomologousArcs { +impl ReduceTo> for IntegralFlowHomologousArcs { type Result = ReductionIFHAToILP; fn reduce_to(&self) -> Result { @@ -123,7 +123,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/integralflowwithmultipliers_ilp.rs b/src/rules/integralflowwithmultipliers_ilp.rs index dc41b9fcf..19d042c5f 100644 --- a/src/rules/integralflowwithmultipliers_ilp.rs +++ b/src/rules/integralflowwithmultipliers_ilp.rs @@ -3,7 +3,7 @@ //! One integer flow variable per arc. Capacity bounds, multiplier-scaled //! conservation at non-terminals, and sink inflow requirement. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowWithMultipliers; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -11,14 +11,14 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing IntegralFlowWithMultipliers to ILP. #[derive(Debug, Clone)] pub struct ReductionIFWMToILP { - target: ILP, + target: ILP, } impl ReductionResult for ReductionIFWMToILP { type Source = IntegralFlowWithMultipliers; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -49,7 +49,7 @@ impl crate::rules::AggregateReductionResult for ReductionIFWMToILP {} num_nonzeros = "num_arcs * (num_arcs + num_vertices - 1)", }, })] -impl ReduceTo> for IntegralFlowWithMultipliers { +impl ReduceTo> for IntegralFlowWithMultipliers { type Result = ReductionIFWMToILP; fn reduce_to(&self) -> Result { @@ -126,7 +126,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/ksatisfiability_feasibleregisterassignment.rs b/src/rules/ksatisfiability_feasibleregisterassignment.rs index 0d2e15fa4..c7e20a609 100644 --- a/src/rules/ksatisfiability_feasibleregisterassignment.rs +++ b/src/rules/ksatisfiability_feasibleregisterassignment.rs @@ -231,7 +231,7 @@ impl ReduceTo for KSatisfiability { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; - use crate::models::algebraic::ILP; + use crate::models::algebraic::{Bounded, ILP}; use crate::models::formula::CNFClause; use crate::solvers::ILPSolver; @@ -248,10 +248,11 @@ pub(crate) fn canonical_rule_example_specs() -> Vec as ReduceTo>::reduce_to(&source) .expect("reduction should succeed"); - let to_ilp = >>::reduce_to( - to_fra.target_problem(), - ) - .expect("reduction should succeed"); + let to_ilp = + >>::reduce_to( + to_fra.target_problem(), + ) + .expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(to_ilp.target_problem()) .expect("canonical FRA example must reduce to a feasible ILP"); diff --git a/src/rules/longestpath_ilp.rs b/src/rules/longestpath_ilp.rs index e2e99c0da..da691c818 100644 --- a/src/rules/longestpath_ilp.rs +++ b/src/rules/longestpath_ilp.rs @@ -5,7 +5,7 @@ //! path positions. Flow-balance constraints force a single directed `s-t` path, //! while MTZ-style ordering constraints eliminate detached cycles. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::LongestPath; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -13,7 +13,7 @@ use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] pub struct ReductionLongestPathToILP { - target: ILP, + target: ILP, num_edges: usize, } @@ -25,9 +25,9 @@ impl ReductionLongestPathToILP { impl ReductionResult for ReductionLongestPathToILP { type Source = LongestPath; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -57,7 +57,7 @@ impl ReductionResult for ReductionLongestPathToILP { num_nonzeros = "(2 * num_edges + num_vertices) * (5 * num_edges + 4 * num_vertices + 1)", }, })] -impl ReduceTo> for LongestPath { +impl ReduceTo> for LongestPath { type Result = ReductionLongestPathToILP; fn reduce_to(&self) -> Result { @@ -89,7 +89,7 @@ impl ReduceTo> for LongestPath { let mut constraints = Vec::new(); - // Directed arc variables are binary within `ILP`. + // Directed arc variables are binary within `ILP`. for edge_idx in 0..num_edges { constraints.push(LinearConstraint::le( vec![(ReductionLongestPathToILP::arc_var(edge_idx, 0), 1)], @@ -197,7 +197,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/maximumleafspanningtree_ilp.rs b/src/rules/maximumleafspanningtree_ilp.rs index 1fac86ae3..49c0d8724 100644 --- a/src/rules/maximumleafspanningtree_ilp.rs +++ b/src/rules/maximumleafspanningtree_ilp.rs @@ -18,7 +18,7 @@ //! //! Objective: maximize sum(z_v) -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumLeafSpanningTree; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -27,15 +27,15 @@ use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MaximumLeafSpanningTree to ILP. #[derive(Debug, Clone)] pub struct ReductionMaximumLeafSpanningTreeToILP { - target: ILP, + target: ILP, num_edges: usize, } impl ReductionResult for ReductionMaximumLeafSpanningTreeToILP { type Source = MaximumLeafSpanningTree; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -64,7 +64,7 @@ impl ReductionResult for ReductionMaximumLeafSpanningTreeToILP { num_nonzeros = "(3 * num_edges + num_vertices) * (3 * num_vertices + 2 * num_edges + 1)", }, })] -impl ReduceTo> for MaximumLeafSpanningTree { +impl ReduceTo> for MaximumLeafSpanningTree { type Result = ReductionMaximumLeafSpanningTreeToILP; fn reduce_to(&self) -> Result { @@ -182,7 +182,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/minimumcapacitatedspanningtree_ilp.rs b/src/rules/minimumcapacitatedspanningtree_ilp.rs index 42f2263e1..515521491 100644 --- a/src/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/rules/minimumcapacitatedspanningtree_ilp.rs @@ -5,7 +5,7 @@ //! - Flow on each edge is bounded by the capacity constraint //! - Flow-edge linking ensures flow only travels on selected edges //! -//! Variable layout (all non-negative integers, `ILP`): +//! Variable layout (all non-negative integers, `ILP`): //! - `y_e` for each undirected edge `e` (indices `0..m`): edge selector (binary) //! - `f_{2e}`, `f_{2e+1}` for each edge `e=(u,v)` (indices `m..3m`): //! directed requirement flow from u to v and v to u respectively @@ -24,7 +24,7 @@ //! //! Objective: minimize sum(w_e * y_e) -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumCapacitatedSpanningTree; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -34,15 +34,15 @@ use crate::types::WeightElement; /// Result of reducing MinimumCapacitatedSpanningTree to ILP. #[derive(Debug, Clone)] pub struct ReductionMinimumCapacitatedSpanningTreeToILP { - target: ILP, + target: ILP, num_edges: usize, } impl ReductionResult for ReductionMinimumCapacitatedSpanningTreeToILP { type Source = MinimumCapacitatedSpanningTree; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -67,7 +67,7 @@ impl ReductionResult for ReductionMinimumCapacitatedSpanningTreeToILP { num_constraints = "5 * num_edges + 2 * num_vertices + 1", num_nonzeros = "(5 * num_edges) * (5 * num_edges + 2 * num_vertices + 1)", })] -impl ReduceTo> for MinimumCapacitatedSpanningTree { +impl ReduceTo> for MinimumCapacitatedSpanningTree { type Result = ReductionMinimumCapacitatedSpanningTreeToILP; fn reduce_to(&self) -> Result { @@ -94,7 +94,7 @@ impl ReduceTo> for MinimumCapacitatedSpanningTree { total.checked_add(requirement.to_sum()).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< MinimumCapacitatedSpanningTree, - ILP, + ILP, >("summing vertex requirements") }) })?; @@ -237,7 +237,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/minimumedgecostflow_ilp.rs b/src/rules/minimumedgecostflow_ilp.rs index 9588f942b..29c3ae0e6 100644 --- a/src/rules/minimumedgecostflow_ilp.rs +++ b/src/rules/minimumedgecostflow_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from MinimumEdgeCostFlow to `ILP`. +//! Reduction from MinimumEdgeCostFlow to `ILP`. //! //! Variables (2m total): //! f_a (a = 0..m-1) — integer flow on arc a, domain {0, ..., c(a)} @@ -18,27 +18,27 @@ //! Objective: minimize Σ p(a) · y_a. //! Extraction: first m variables are the flow values. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumEdgeCostFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing MinimumEdgeCostFlow to `ILP`. +/// Result of reducing MinimumEdgeCostFlow to `ILP`. /// /// Variable layout: /// - `f_a` at index a for a in 0..num_edges (flow on arc a) /// - `y_a` at index num_edges + a for a in 0..num_edges (binary indicator) #[derive(Debug, Clone)] pub struct ReductionMECFToILP { - target: ILP, + target: ILP, num_edges: usize, } impl ReductionResult for ReductionMECFToILP { type Source = MinimumEdgeCostFlow; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -62,7 +62,7 @@ impl ReductionResult for ReductionMECFToILP { num_nonzeros = "(2 * num_edges) * (2 * num_edges + num_vertices - 1)", }, })] -impl ReduceTo> for MinimumEdgeCostFlow { +impl ReduceTo> for MinimumEdgeCostFlow { type Result = ReductionMECFToILP; fn reduce_to(&self) -> Result { @@ -160,7 +160,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/minimumfeedbackarcset_ilp.rs b/src/rules/minimumfeedbackarcset_ilp.rs index b2f236d5b..f91edd92a 100644 --- a/src/rules/minimumfeedbackarcset_ilp.rs +++ b/src/rules/minimumfeedbackarcset_ilp.rs @@ -9,14 +9,14 @@ //! - Objective: Minimize Σ w_a * y_a //! - Variable layout: first |A| are y_a, next |V| are o_v -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumFeedbackArcSet; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing MinimumFeedbackArcSet to ILP. /// -/// The ILP uses integer variables (`ILP`) because it needs both +/// The ILP uses integer variables (`ILP`) because it needs both /// binary arc-removal variables (y_a) and integer ordering variables (o_v). /// /// Variable layout: @@ -24,16 +24,16 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// - `o_v` at index `m + v` for `v in 0..n`: integer in {0, ..., n-1}, topological order #[derive(Debug, Clone)] pub struct ReductionFASToILP { - target: ILP, + target: ILP, /// Number of arcs in the source graph (needed for solution extraction). num_arcs: usize, } impl ReductionResult for ReductionFASToILP { type Source = MinimumFeedbackArcSet; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -63,7 +63,7 @@ impl ReductionResult for ReductionFASToILP { num_nonzeros = "(num_arcs + num_vertices) * (num_arcs + num_arcs + num_vertices)", }, })] -impl ReduceTo> for MinimumFeedbackArcSet { +impl ReduceTo> for MinimumFeedbackArcSet { type Result = ReductionFASToILP; fn reduce_to(&self) -> Result { @@ -139,7 +139,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/minimumfeedbackvertexset_ilp.rs b/src/rules/minimumfeedbackvertexset_ilp.rs index b6e0b88e7..e2747758d 100644 --- a/src/rules/minimumfeedbackvertexset_ilp.rs +++ b/src/rules/minimumfeedbackvertexset_ilp.rs @@ -6,14 +6,14 @@ //! Plus binary bounds (x_i <= 1) and order bounds (o_i <= n-1) //! - Objective: Minimize the weighted sum of removed vertices -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumFeedbackVertexSet; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing MinimumFeedbackVertexSet to ILP. /// -/// The ILP uses integer variables (`ILP`) because it needs both +/// The ILP uses integer variables (`ILP`) because it needs both /// binary selection variables (x_i) and integer ordering variables (o_i). /// /// Variable layout: @@ -21,16 +21,16 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// - `o_i` at index `n + i` for `i in 0..n`: integer in {0, ..., n-1}, topological order #[derive(Debug, Clone)] pub struct ReductionMFVSToILP { - target: ILP, + target: ILP, /// Number of vertices in the source graph (needed for solution extraction). num_vertices: usize, } impl ReductionResult for ReductionMFVSToILP { type Source = MinimumFeedbackVertexSet; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -60,7 +60,7 @@ impl ReductionResult for ReductionMFVSToILP { num_nonzeros = "(2 * num_vertices) * (num_arcs + 2 * num_vertices)", }, })] -impl ReduceTo> for MinimumFeedbackVertexSet { +impl ReduceTo> for MinimumFeedbackVertexSet { type Result = ReductionMFVSToILP; fn reduce_to(&self) -> Result { @@ -73,7 +73,10 @@ impl ReduceTo> for MinimumFeedbackVertexSet { // o_i = n + i (integer: topological order of vertex i) let mut constraints = Vec::new(); - let n_i64 = >>::exact_i64(n, "encoding the topological order")?; + let n_i64 = >>::exact_i64( + n, + "encoding the topological order", + )?; // Binary bounds: x_i <= 1 for i in 0..n for i in 0..n { @@ -109,12 +112,12 @@ impl ReduceTo> for MinimumFeedbackVertexSet { let mut variables = vec![IntegerVariable::binary(); num_vars]; variables[n..].fill( IntegerVariable::new(Some(0), Some((n_i64 - 1).max(0))) - .map_err(>>::target_construction)?, + .map_err(>>::target_construction)?, ); let target = ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) - .map_err(>>::target_construction)?; + .map_err(>>::target_construction)?; Ok(ReductionMFVSToILP { target, @@ -133,7 +136,7 @@ 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]); - crate::example_db::specs::rule_example_via_ilp::<_, i64>(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/minimumgraphbandwidth_ilp.rs b/src/rules/minimumgraphbandwidth_ilp.rs index 9dc8f2914..e4ed07ec1 100644 --- a/src/rules/minimumgraphbandwidth_ilp.rs +++ b/src/rules/minimumgraphbandwidth_ilp.rs @@ -7,7 +7,7 @@ //! - For each edge (u,v): pos_u - pos_v <= B, pos_v - pos_u <= B //! - Objective: minimize B -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumGraphBandwidth; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -15,21 +15,21 @@ use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinimumGraphBandwidth to ILP. /// -/// Variable layout (`ILP`, non-negative integers): +/// Variable layout (`ILP`, non-negative integers): /// - `x_{v,p}` at index `v * n + p`, bounded to {0,1} /// - `pos_v` at index `n^2 + v`, integer position in {0, ..., n-1} /// - `B` (bandwidth) at index `n^2 + n` #[derive(Debug, Clone)] pub struct ReductionMGBToILP { - target: ILP, + target: ILP, num_vertices: usize, } impl ReductionResult for ReductionMGBToILP { type Source = MinimumGraphBandwidth; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -58,7 +58,7 @@ impl ReductionResult for ReductionMGBToILP { num_nonzeros = "(num_vertices^2 + num_vertices + 1) * (2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 1 + 2 * num_edges)", }, })] -impl ReduceTo> for MinimumGraphBandwidth { +impl ReduceTo> for MinimumGraphBandwidth { type Result = ReductionMGBToILP; fn reduce_to(&self) -> Result { @@ -88,7 +88,7 @@ impl ReduceTo> for MinimumGraphBandwidth { constraints.push(LinearConstraint::eq(terms, 1)); } - // Binary bounds for x variables (`ILP`) + // Binary bounds for x variables (`ILP`) for v in 0..n { for p in 0..n { constraints.push(LinearConstraint::le(vec![(x_idx(v, p), 1)], 1)); @@ -156,7 +156,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/minimumweightdecoding_ilp.rs b/src/rules/minimumweightdecoding_ilp.rs index 606fe757d..f6f643d9a 100644 --- a/src/rules/minimumweightdecoding_ilp.rs +++ b/src/rules/minimumweightdecoding_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from MinimumWeightDecoding to `ILP`. +//! Reduction from MinimumWeightDecoding to `ILP`. //! //! The GF(2) constraint Hx ≡ s (mod 2) is linearized by introducing integer //! slack variables k_i for each row: @@ -16,26 +16,26 @@ //! Objective: minimize Σ x_j (Hamming weight). use crate::models::algebraic::MinimumWeightDecoding; -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing MinimumWeightDecoding to `ILP`. +/// Result of reducing MinimumWeightDecoding to `ILP`. /// /// Variable layout: /// - x_j at index j for j in 0..num_cols (binary codeword bits) /// - k_i at index num_cols + i for i in 0..num_rows (integer slack) #[derive(Debug, Clone)] pub struct ReductionMinimumWeightDecodingToILP { - target: ILP, + target: ILP, num_cols: usize, } impl ReductionResult for ReductionMinimumWeightDecodingToILP { type Source = MinimumWeightDecoding; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -62,7 +62,7 @@ impl ReductionResult for ReductionMinimumWeightDecodingToILP { num_nonzeros = "(num_cols + num_rows) * (num_rows + num_cols)", }, })] -impl ReduceTo> for MinimumWeightDecoding { +impl ReduceTo> for MinimumWeightDecoding { type Result = ReductionMinimumWeightDecodingToILP; fn reduce_to(&self) -> Result { @@ -131,7 +131,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/minmaxmulticenter_ilp.rs b/src/rules/minmaxmulticenter_ilp.rs index 82b521f56..a8eff69bc 100644 --- a/src/rules/minmaxmulticenter_ilp.rs +++ b/src/rules/minmaxmulticenter_ilp.rs @@ -1,7 +1,7 @@ //! Reduction from MinMaxMulticenter to ILP (Integer Linear Programming). //! //! The vertex p-center optimization problem is formulated as a mixed ILP -//! using `ILP` to accommodate both binary and integer variables. +//! using `ILP` to accommodate both binary and integer variables. //! //! Variable layout: //! - `x_j` for each vertex j (binary: 1 if vertex j is selected as a center), indices `0..n` @@ -14,7 +14,7 @@ //! - Assignment: ∀i: Σ_j y_{i,j} = 1 (each vertex assigned to exactly one center) //! - Assignment link: ∀i,j: if j is reachable from i then y_{i,j} ≤ x_j, //! otherwise y_{i,j} = 0 -//! - Binary bounds: x_j ≤ 1, y_{i,j} ≤ 1 (enforce binary within `ILP`) +//! - Binary bounds: x_j ≤ 1, y_{i,j} ≤ 1 (enforce binary within `ILP`) //! - Minimax: ∀i: Σ_j w_i · d(i,j) · y_{i,j} ≤ z //! //! Objective: minimize z. @@ -24,7 +24,7 @@ //! Note: All-pairs shortest-path distances are computed using weighted shortest //! paths over `edge_lengths`. Unreachable assignment variables are forced to 0. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinMaxMulticenter; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -33,15 +33,15 @@ use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinMaxMulticenter to ILP. #[derive(Debug, Clone)] pub struct ReductionMMCToILP { - target: ILP, + target: ILP, num_vertices: usize, } impl ReductionResult for ReductionMMCToILP { type Source = MinMaxMulticenter; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -130,7 +130,7 @@ fn weighted_distances_mmc( num_nonzeros = "(num_vertices + num_vertices^2 + 1) * (2 * num_vertices^2 + 3 * num_vertices + 2)", }, })] -impl ReduceTo> for MinMaxMulticenter { +impl ReduceTo> for MinMaxMulticenter { type Result = ReductionMMCToILP; fn reduce_to(&self) -> Result { @@ -177,7 +177,7 @@ impl ReduceTo> for MinMaxMulticenter { } } - // Binary bounds for x_j and y_{i,j} (enforce binary within `ILP`) + // Binary bounds for x_j and y_{i,j} (enforce binary within `ILP`) for j in 0..n { constraints.push(LinearConstraint::le(vec![(x_var(j), 1)], 1)); } @@ -200,7 +200,7 @@ impl ReduceTo> for MinMaxMulticenter { vertex_weights[i].checked_mul(distance).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< MinMaxMulticenter, - ILP, + ILP, >( "multiplying a vertex weight by a shortest-path distance" ) @@ -259,7 +259,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/mixedchinesepostman_ilp.rs b/src/rules/mixedchinesepostman_ilp.rs index a12819f2d..5a6b77cb1 100644 --- a/src/rules/mixedchinesepostman_ilp.rs +++ b/src/rules/mixedchinesepostman_ilp.rs @@ -5,7 +5,7 @@ //! within the length bound. Uses connectivity flow constraints on both //! forward and reverse directions. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MixedChinesePostman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -14,15 +14,15 @@ use crate::types::WeightElement; /// Result of reducing MixedChinesePostman to ILP. #[derive(Debug, Clone)] pub struct ReductionMCPToILP { - target: ILP, + target: ILP, num_undirected_edges: usize, } impl ReductionResult for ReductionMCPToILP { type Source = MixedChinesePostman; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -47,7 +47,7 @@ impl ReductionResult for ReductionMCPToILP { num_constraints = "num_edges + 8 * (num_arcs + 2 * num_edges) + 10 * num_vertices + 2", num_nonzeros = "(num_edges + 4 * (num_arcs + 2 * num_edges) + 3 * num_vertices + 1) * (num_edges + 8 * (num_arcs + 2 * num_edges) + 10 * num_vertices + 2)", })] -impl ReduceTo> for MixedChinesePostman { +impl ReduceTo> for MixedChinesePostman { type Result = ReductionMCPToILP; #[allow(clippy::needless_range_loop)] @@ -115,14 +115,16 @@ impl ReduceTo> for MixedChinesePostman { let n_i64 = Self::exact_i64(n, "encoding the active-vertex count")?; let r_count_i64 = Self::exact_i64(r_count, "encoding the required-arc count")?; let big_g = r_count_i64.checked_mul(n_i64 - 1).ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::, ILP>( - "computing the extra-traversal bound", - ) + crate::rules::ReductionError::integer_overflow::< + MixedChinesePostman, + ILP, + >("computing the extra-traversal bound") })?; let m_use = big_g.checked_add(1).ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::, ILP>( - "computing the arc-use bound", - ) + crate::rules::ReductionError::integer_overflow::< + MixedChinesePostman, + ILP, + >("computing the arc-use bound") })?; let mut constraints = Vec::new(); @@ -293,7 +295,7 @@ impl ReduceTo> for MixedChinesePostman { vec![(b_idx(v), 1), (s_idx, -1), (rho_idx(v), -n_i64)], -n_i64, )); - // b_v >= 0 is implied by `ILP` non-negativity + // b_v >= 0 is implied by `ILP` non-negativity } // Flow bounds: 0 <= f_j, h_j <= (n-1) * y_j @@ -400,7 +402,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/mod.rs b/src/rules/mod.rs index a55e411e4..2251492c5 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -191,6 +191,7 @@ pub(crate) mod graphpartitioning_ilp; pub(crate) mod hamiltonianpath_ilp; pub(crate) mod highlyconnecteddeletion_ilp; mod ilp_bool_ilp_i64; +mod ilp_bounded_ilp; pub(crate) mod ilp_helpers; pub(crate) mod ilp_qubo; pub(crate) mod integralflowbundles_ilp; diff --git a/src/rules/multiplechoicebranching_ilp.rs b/src/rules/multiplechoicebranching_ilp.rs index 63e8b1121..8a63e05fc 100644 --- a/src/rules/multiplechoicebranching_ilp.rs +++ b/src/rules/multiplechoicebranching_ilp.rs @@ -1,19 +1,19 @@ //! Reduction from MultipleChoiceBranching with integer weights to integer ILP. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MultipleChoiceBranching; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionMultipleChoiceBranchingToILP { - target: ILP, + target: ILP, num_arcs: usize, } impl ReductionResult for ReductionMultipleChoiceBranchingToILP { type Source = MultipleChoiceBranching; - type Target = ILP; + type Target = ILP; fn target_problem(&self) -> &Self::Target { &self.target @@ -48,7 +48,7 @@ impl crate::rules::AggregateReductionResult for ReductionMultipleChoiceBranching num_nonzeros = "(num_arcs + num_vertices) * (2 * num_arcs + 2 * num_vertices + num_partition_groups + 1)", }, })] -impl ReduceTo> for MultipleChoiceBranching { +impl ReduceTo> for MultipleChoiceBranching { type Result = ReductionMultipleChoiceBranchingToILP; fn reduce_to(&self) -> Result { @@ -61,7 +61,7 @@ impl ReduceTo> for MultipleChoiceBranching { constraints.push(LinearConstraint::le(vec![(arc, 1)], 1)); } if num_vertices > 0 { - let big_m = >>::exact_i64( + let big_m = >>::exact_i64( num_vertices, "encoding topological-order constraints", )?; @@ -114,7 +114,7 @@ impl ReduceTo> for MultipleChoiceBranching { ); let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) - .map_err(>>::target_construction)?; + .map_err(>>::target_construction)?; Ok(ReductionMultipleChoiceBranchingToILP { target, num_arcs }) } } @@ -132,7 +132,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/openshopscheduling_ilp.rs b/src/rules/openshopscheduling_ilp.rs index 514ff218a..67819b545 100644 --- a/src/rules/openshopscheduling_ilp.rs +++ b/src/rules/openshopscheduling_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from OpenShopScheduling to `ILP`. +//! Reduction from OpenShopScheduling to `ILP`. //! //! Disjunctive formulation with binary ordering variables and integer start times: //! @@ -26,13 +26,13 @@ //! //! **Objective:** Minimize C. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::OpenShopScheduling; use crate::models::Decision; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing OpenShopScheduling to `ILP`. +/// Result of reducing OpenShopScheduling to `ILP`. /// /// Variable layout: /// - `x_{j,k,i}` at index `pair_idx(j,k) * m + i` (num_pairs * m vars) @@ -42,7 +42,7 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// - `C`: at index `num_order_vars + n * m + n * m*(m-1)/2` (1 var) #[derive(Debug, Clone)] pub struct ReductionOSSToILP { - target: ILP, + target: ILP, num_jobs: usize, num_machines: usize, /// n*(n-1)/2 * m — start index of s_{j,i} variables @@ -87,9 +87,9 @@ impl ReductionOSSToILP { impl ReductionResult for ReductionOSSToILP { type Source = OpenShopScheduling; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -112,7 +112,7 @@ impl ReductionResult for ReductionOSSToILP { num_nonzeros = "(num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 1) * (num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + 1 + 2 * num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 2 * num_jobs * num_machines * (num_machines - 1) / 2 + num_jobs * num_machines)", }, })] -impl ReduceTo> for OpenShopScheduling { +impl ReduceTo> for OpenShopScheduling { type Result = ReductionOSSToILP; fn reduce_to(&self) -> Result { @@ -143,9 +143,10 @@ impl ReduceTo> for OpenShopScheduling { .flat_map(|row| row.iter()) .try_fold(0_i64, |total, &time| total.checked_add(time)) .ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::>( - "summing open-shop processing times", - ) + crate::rules::ReductionError::integer_overflow::< + OpenShopScheduling, + ILP, + >("summing open-shop processing times") })?; let big_m = total_p; let processing_times = p; @@ -296,7 +297,7 @@ pub struct ReductionDecisionOpenShopSchedulingToILP { impl ReductionResult for ReductionDecisionOpenShopSchedulingToILP { type Source = Decision; - type Target = ILP; + type Target = ILP; fn target_problem(&self) -> &Self::Target { self.inner.target_problem() @@ -325,11 +326,11 @@ impl crate::rules::AggregateReductionResult for ReductionDecisionOpenShopSchedul num_nonzeros = "(num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 1) * (3 * num_jobs * (num_jobs - 1) / 2 * num_machines + 2 * num_jobs * num_machines + 3 * num_jobs * num_machines * (num_machines - 1) / 2 + 2)", }, })] -impl ReduceTo> for Decision { +impl ReduceTo> for Decision { type Result = ReductionDecisionOpenShopSchedulingToILP; fn reduce_to(&self) -> Result { - let mut inner = ReduceTo::>::reduce_to(self.inner())?; + let mut inner = ReduceTo::>::reduce_to(self.inner())?; let mut constraints = inner.target.constraints().to_vec(); constraints.push(LinearConstraint::le( inner.target.objective().to_vec(), @@ -341,7 +342,7 @@ impl ReduceTo> for Decision { vec![], ObjectiveSense::Minimize, ) - .map_err(>>::target_construction)?; + .map_err(>>::target_construction)?; Ok(ReductionDecisionOpenShopSchedulingToILP { inner }) } } @@ -354,7 +355,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }, crate::example_db::specs::RuleExampleSpec { @@ -362,7 +363,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }, ] diff --git a/src/rules/optimallineararrangement_ilp.rs b/src/rules/optimallineararrangement_ilp.rs index e37b18c83..dfd67cf74 100644 --- a/src/rules/optimallineararrangement_ilp.rs +++ b/src/rules/optimallineararrangement_ilp.rs @@ -7,7 +7,7 @@ //! - abs_diff_le constraints: z_{u,v} >= p_u - p_v, z_{u,v} >= p_v - p_u //! - Minimize: sum z_{u,v} -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::OptimalLinearArrangement; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -15,21 +15,21 @@ use crate::topology::{Graph, SimpleGraph}; /// Result of reducing OptimalLinearArrangement to ILP. /// -/// Variable layout (`ILP`, non-negative integers): +/// Variable layout (`ILP`, non-negative integers): /// - `x_{v,p}` at index `v * n + p`, bounded to {0,1} /// - `p_v` at index `n^2 + v`, integer position in {0, ..., n-1} /// - `z_e` at index `n^2 + n + e`, non-negative integer for edge length #[derive(Debug, Clone)] pub struct ReductionOLAToILP { - target: ILP, + target: ILP, num_vertices: usize, } impl ReductionResult for ReductionOLAToILP { type Source = OptimalLinearArrangement; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -58,7 +58,7 @@ impl ReductionResult for ReductionOLAToILP { num_nonzeros = "(num_vertices^2 + num_vertices + num_edges) * (2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 3 * num_edges)", }, })] -impl ReduceTo> for OptimalLinearArrangement { +impl ReduceTo> for OptimalLinearArrangement { type Result = ReductionOLAToILP; fn reduce_to(&self) -> Result { @@ -75,7 +75,8 @@ impl ReduceTo> for OptimalLinearArrangement { let z_idx = |e: usize| -> usize { num_x + n + e }; let mut constraints = Vec::new(); - let n_i64 = >>::exact_i64(n, "encoding a vertex position")?; + let n_i64 = + >>::exact_i64(n, "encoding a vertex position")?; // Assignment: each vertex in exactly one position for v in 0..n { @@ -89,7 +90,7 @@ impl ReduceTo> for OptimalLinearArrangement { constraints.push(LinearConstraint::eq(terms, 1)); } - // Binary bounds for x variables (`ILP`) + // Binary bounds for x variables (`ILP`) for v in 0..n { for p in 0..n { constraints.push(LinearConstraint::le(vec![(x_idx(v, p), 1)], 1)); @@ -103,7 +104,10 @@ impl ReduceTo> for OptimalLinearArrangement { for p in 0..n { terms.push(( x_idx(v, p), - ->>::exact_i64(p, "encoding a vertex position")?, + ->>::exact_i64( + p, + "encoding a vertex position", + )?, )); } constraints.push(LinearConstraint::eq(terms, 0)); @@ -135,12 +139,12 @@ impl ReduceTo> for OptimalLinearArrangement { let mut variables = vec![IntegerVariable::binary(); num_vars]; variables[num_x..].fill( IntegerVariable::new(Some(0), Some((n_i64 - 1).max(0))) - .map_err(>>::target_construction)?, + .map_err(>>::target_construction)?, ); let target = ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) - .map_err(>>::target_construction)?; + .map_err(>>::target_construction)?; Ok(ReductionOLAToILP { target, @@ -157,7 +161,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/pathconstrainednetworkflow_ilp.rs b/src/rules/pathconstrainednetworkflow_ilp.rs index 635030112..99383217c 100644 --- a/src/rules/pathconstrainednetworkflow_ilp.rs +++ b/src/rules/pathconstrainednetworkflow_ilp.rs @@ -3,7 +3,7 @@ //! One integer variable per prescribed path. Arc capacity aggregation //! across paths and total flow requirement. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::PathConstrainedNetworkFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -11,14 +11,14 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing PathConstrainedNetworkFlow to ILP. #[derive(Debug, Clone)] pub struct ReductionPCNFToILP { - target: ILP, + target: ILP, } impl ReductionResult for ReductionPCNFToILP { type Source = PathConstrainedNetworkFlow; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -45,7 +45,7 @@ impl crate::rules::AggregateReductionResult for ReductionPCNFToILP {} num_constraints = "num_arcs + 1", num_nonzeros = "num_paths * (num_arcs + 1)", })] -impl ReduceTo> for PathConstrainedNetworkFlow { +impl ReduceTo> for PathConstrainedNetworkFlow { type Result = ReductionPCNFToILP; fn reduce_to(&self) -> Result { @@ -102,7 +102,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/preemptivescheduling_ilp.rs b/src/rules/preemptivescheduling_ilp.rs index badad6f82..e66317261 100644 --- a/src/rules/preemptivescheduling_ilp.rs +++ b/src/rules/preemptivescheduling_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from PreemptiveScheduling to `ILP`. +//! Reduction from PreemptiveScheduling to `ILP`. //! //! Time-indexed formulation with an auxiliary integer makespan variable: //! - Variables: binary x_{t,u} for t in 0..n, u in 0..D_max (task t processed at slot u), @@ -15,18 +15,18 @@ //! 4. Makespan lower bound: M ≥ (u+1) when x_{t,u}=1: //! `M - (u+1)*x_{t,u} ≥ 0` for all t,u //! 5. Binary bounds: x_{t,u} ≤ 1 for each t,u -//! (since `ILP` uses non-negative integer domain) +//! (since `ILP` uses non-negative integer domain) //! - Objective: Minimize M. //! -//! Note: `ILP` treats all variables as non-negative integers. Binary constraints +//! Note: `ILP` treats all variables as non-negative integers. Binary constraints //! on x_{t,u} are enforced by x_{t,u} ≤ 1. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::PreemptiveScheduling; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing PreemptiveScheduling to `ILP`. +/// Result of reducing PreemptiveScheduling to `ILP`. /// /// Variable layout: /// - x_{t,u} at index t * D_max + u for t in 0..n, u in 0..D_max (n*D_max vars) @@ -35,16 +35,16 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// Total: n * D_max + 1 variables. #[derive(Debug, Clone)] pub struct ReductionPSToILP { - target: ILP, + target: ILP, num_tasks: usize, d_max: usize, } impl ReductionResult for ReductionPSToILP { type Source = PreemptiveScheduling; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -76,7 +76,7 @@ impl ReductionResult for ReductionPSToILP { num_nonzeros = "(num_tasks * d_max + 1) * (num_tasks + d_max + num_precedences * d_max + 2 * num_tasks * d_max)", }, })] -impl ReduceTo> for PreemptiveScheduling { +impl ReduceTo> for PreemptiveScheduling { type Result = ReductionPSToILP; fn reduce_to(&self) -> Result { @@ -178,7 +178,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/registersufficiency_ilp.rs b/src/rules/registersufficiency_ilp.rs index c5595f662..b88b6d503 100644 --- a/src/rules/registersufficiency_ilp.rs +++ b/src/rules/registersufficiency_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from RegisterSufficiency to `ILP`. +//! Reduction from RegisterSufficiency to `ILP`. //! //! The formulation uses: //! - integer `t_v` variables for evaluation positions @@ -7,22 +7,22 @@ //! - binary threshold/live indicators to count how many values are live after //! each evaluation step -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::RegisterSufficiency; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionRegisterSufficiencyToILP { - target: ILP, + target: ILP, num_vertices: usize, } impl ReductionResult for ReductionRegisterSufficiencyToILP { type Source = RegisterSufficiency; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -51,7 +51,7 @@ impl crate::rules::AggregateReductionResult for ReductionRegisterSufficiencyToIL num_nonzeros = "18 * num_vertices^2 + 2 * num_vertices + 7 * num_vertices * (num_vertices - 1) / 2 + 4 * num_arcs + num_sinks", }, )] -impl ReduceTo> for RegisterSufficiency { +impl ReduceTo> for RegisterSufficiency { type Result = ReductionRegisterSufficiencyToILP; fn reduce_to(&self) -> Result { @@ -219,7 +219,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/rootedtreestorageassignment_ilp.rs b/src/rules/rootedtreestorageassignment_ilp.rs index dce49eff9..b673dbeb9 100644 --- a/src/rules/rootedtreestorageassignment_ilp.rs +++ b/src/rules/rootedtreestorageassignment_ilp.rs @@ -4,7 +4,7 @@ //! a_{u,v}, transitive-closure helpers h_{u,v,w}, and per-subset gadgets //! (top/bottom selectors, pair selectors, endpoint depths, extension costs). -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::RootedTreeStorageAssignment; use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode_rows}; @@ -59,15 +59,15 @@ fn total_vars(n: usize, r: usize) -> usize { #[derive(Debug, Clone)] pub struct ReductionRTSAToILP { - target: ILP, + target: ILP, n: usize, } impl ReductionResult for ReductionRTSAToILP { type Source = RootedTreeStorageAssignment; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -95,7 +95,7 @@ impl crate::rules::AggregateReductionResult for ReductionRTSAToILP {} num_constraints = "4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8)", num_nonzeros = "(universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)) * (4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8))", })] -impl ReduceTo> for RootedTreeStorageAssignment { +impl ReduceTo> for RootedTreeStorageAssignment { type Result = ReductionRTSAToILP; fn reduce_to(&self) -> Result { @@ -407,7 +407,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source) + .expect("reduction should succeed"); let target_config = { let ilp_solver = crate::solvers::ILPSolver::new(); ilp_solver @@ -415,7 +416,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( + crate::example_db::specs::rule_example_with_witness::<_, ILP>( source, SolutionPair { source_config: serde_json::to_value(source_config) diff --git a/src/rules/ruralpostman_ilp.rs b/src/rules/ruralpostman_ilp.rs index f056d2f7a..287153612 100644 --- a/src/rules/ruralpostman_ilp.rs +++ b/src/rules/ruralpostman_ilp.rs @@ -4,7 +4,7 @@ //! connectivity flow constraints to encode an Eulerian connected subgraph //! covering all required edges within the length bound. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::RuralPostman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -14,15 +14,15 @@ use crate::types::WeightElement; /// Result of reducing RuralPostman to ILP. #[derive(Debug, Clone)] pub struct ReductionRPToILP { - target: ILP, + target: ILP, num_edges: usize, } impl ReductionResult for ReductionRPToILP { type Source = RuralPostman; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -49,7 +49,7 @@ impl ReductionResult for ReductionRPToILP { num_nonzeros = "(num_edges + num_vertices + num_edges + num_vertices + 2 * num_edges) * (2 * num_edges + num_required_edges + num_vertices + 2 * num_edges + num_vertices + 2 * num_edges + num_vertices + num_edges + num_edges + num_vertices)", }, })] -impl ReduceTo> for RuralPostman { +impl ReduceTo> for RuralPostman { type Result = ReductionRPToILP; fn reduce_to(&self) -> Result { @@ -239,7 +239,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs index 9b5aaf957..cb535f1ac 100644 --- a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs @@ -5,7 +5,7 @@ //! ordering variables `y_{i,j}` for each task pair. Big-M constraints //! enforce that tasks sharing a processor do not overlap. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SchedulingToMinimizeWeightedCompletionTime; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; @@ -22,7 +22,7 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// Total variables: n*m + n + n*(n-1)/2 #[derive(Debug, Clone)] pub struct ReductionSMWCTToILP { - target: ILP, + target: ILP, num_tasks: usize, num_processors: usize, } @@ -45,9 +45,9 @@ impl ReductionSMWCTToILP { impl ReductionResult for ReductionSMWCTToILP { type Source = SchedulingToMinimizeWeightedCompletionTime; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -71,7 +71,7 @@ impl ReductionResult for ReductionSMWCTToILP { num_nonzeros = "(num_tasks * num_processors + num_tasks + num_tasks * (num_tasks - 1) / 2) * (num_tasks + num_tasks * num_processors + 2 * num_tasks + 2 * num_tasks * (num_tasks - 1) / 2 * num_processors + num_tasks * (num_tasks - 1) / 2)", }, })] -impl ReduceTo> for SchedulingToMinimizeWeightedCompletionTime { +impl ReduceTo> for SchedulingToMinimizeWeightedCompletionTime { type Result = ReductionSMWCTToILP; fn reduce_to(&self) -> Result { @@ -85,7 +85,7 @@ impl ReduceTo> for SchedulingToMinimizeWeightedCompletionTime { .ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< SchedulingToMinimizeWeightedCompletionTime, - ILP, + ILP, >("summing task processing times") })?; let lengths = self.lengths(); @@ -94,13 +94,13 @@ impl ReduceTo> for SchedulingToMinimizeWeightedCompletionTime { let two_big_m = big_m.checked_mul(2).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< SchedulingToMinimizeWeightedCompletionTime, - ILP, + ILP, >("doubling the disjunctive scheduling bound") })?; let three_big_m = big_m.checked_mul(3).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< SchedulingToMinimizeWeightedCompletionTime, - ILP, + ILP, >("tripling the disjunctive scheduling bound") })?; @@ -218,7 +218,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs index 2e5bc5947..1307eef41 100644 --- a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs +++ b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs @@ -1,16 +1,16 @@ -//! Reduction from SequencingToMinimizeMaximumCumulativeCost to `ILP`. +//! Reduction from SequencingToMinimizeMaximumCumulativeCost to `ILP`. //! //! Position-assignment ILP: binary x_{j,p} placing task j in position p. //! Permutation constraints, precedence constraints, and prefix cumulative-cost //! bounds at every position. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeMaximumCumulativeCost; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing SequencingToMinimizeMaximumCumulativeCost to `ILP`. +/// Result of reducing SequencingToMinimizeMaximumCumulativeCost to `ILP`. /// /// Variable layout: /// - x_{j,p} for j in 0..n, p in 0..n: index `j*n + p` @@ -18,15 +18,15 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// Total: n^2 variables. #[derive(Debug, Clone)] pub struct ReductionSTMMCCToILP { - target: ILP, + target: ILP, num_tasks: usize, } impl ReductionResult for ReductionSTMMCCToILP { type Source = SequencingToMinimizeMaximumCumulativeCost; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -54,7 +54,7 @@ impl ReductionResult for ReductionSTMMCCToILP { num_nonzeros = "(num_tasks^2 + 1) * (num_tasks^2 + 3 * num_tasks + num_precedences + 1)", }, })] -impl ReduceTo> for SequencingToMinimizeMaximumCumulativeCost { +impl ReduceTo> for SequencingToMinimizeMaximumCumulativeCost { type Result = ReductionSTMMCCToILP; fn reduce_to(&self) -> Result { @@ -90,7 +90,7 @@ impl ReduceTo> for SequencingToMinimizeMaximumCumulativeCost { constraints.push(LinearConstraint::ge(terms, 1)); } - // Binary bounds for x variables (`ILP` allows any non-negative integer) + // Binary bounds for x variables (`ILP` allows any non-negative integer) for j in 0..n { for p in 0..n { constraints.push(LinearConstraint::le(vec![(x_var(j, p), 1)], 1)); @@ -116,13 +116,13 @@ impl ReduceTo> for SequencingToMinimizeMaximumCumulativeCost { let magnitude = cost.checked_abs().ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< SequencingToMinimizeMaximumCumulativeCost, - ILP, + ILP, >("taking the absolute value of a task cost") })?; total.checked_add(magnitude).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< SequencingToMinimizeMaximumCumulativeCost, - ILP, + ILP, >("summing absolute task costs") }) })?; @@ -155,7 +155,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index d0e1f4a4e..46ab83739 100644 --- a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -1,18 +1,18 @@ //! Reduction from SequencingToMinimizeWeightedCompletionTime to ILP. //! //! The reduction uses integer completion-time variables `C_j` and integer -//! order variables `y_{i,j}` constrained to `{0, 1}` within `ILP`. +//! order variables `y_{i,j}` constrained to `{0, 1}` within `ILP`. //! For each unordered pair `{i, j}`, a pair of big-M constraints forces one //! task to finish before the other starts. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionSTMWCTToILP { - target: ILP, + target: ILP, num_tasks: usize, } @@ -31,9 +31,9 @@ impl ReductionSTMWCTToILP { impl ReductionResult for ReductionSTMWCTToILP { type Source = SequencingToMinimizeWeightedCompletionTime; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -60,7 +60,7 @@ impl ReductionResult for ReductionSTMWCTToILP { num_nonzeros = "(num_tasks + num_tasks * (num_tasks - 1) / 2) * (2 * num_tasks + 3 * num_tasks * (num_tasks - 1) / 2 + num_precedences)", }, })] -impl ReduceTo> for SequencingToMinimizeWeightedCompletionTime { +impl ReduceTo> for SequencingToMinimizeWeightedCompletionTime { type Result = ReductionSTMWCTToILP; fn reduce_to(&self) -> Result { @@ -70,7 +70,7 @@ impl ReduceTo> for SequencingToMinimizeWeightedCompletionTime { total.checked_add(length).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< SequencingToMinimizeWeightedCompletionTime, - ILP, + ILP, >("summing task processing times") }) })?; @@ -158,7 +158,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs index de1d41b38..928b711e5 100644 --- a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -1,15 +1,15 @@ -//! Reduction from SequencingToMinimizeWeightedTardiness to `ILP`. +//! Reduction from SequencingToMinimizeWeightedTardiness to `ILP`. //! //! Pairwise order variables y_{i,j}, integer completion times C_j, //! and nonnegative tardiness variables T_j. Big-M disjunctive constraints //! force a single-machine order; the weighted tardiness sum is bounded by K. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedTardiness; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing SequencingToMinimizeWeightedTardiness to `ILP`. +/// Result of reducing SequencingToMinimizeWeightedTardiness to `ILP`. /// /// Variable layout: /// - `y_{i,j}` for i < j: pairwise order bits (n*(n-1)/2 vars) @@ -19,16 +19,16 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// Total: n*(n-1)/2 + 2*n variables. #[derive(Debug, Clone)] pub struct ReductionSTMWTToILP { - target: ILP, + target: ILP, num_tasks: usize, num_order_vars: usize, } impl ReductionResult for ReductionSTMWTToILP { type Source = SequencingToMinimizeWeightedTardiness; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -62,7 +62,7 @@ impl crate::rules::AggregateReductionResult for ReductionSTMWTToILP {} num_constraints = "2 * num_tasks^2 + 3 * num_tasks + 1", num_nonzeros = "(num_tasks^2 + 2 * num_tasks) * (2 * num_tasks^2 + 3 * num_tasks + 1)", })] -impl ReduceTo> for SequencingToMinimizeWeightedTardiness { +impl ReduceTo> for SequencingToMinimizeWeightedTardiness { type Result = ReductionSTMWTToILP; fn reduce_to(&self) -> Result { @@ -89,7 +89,7 @@ impl ReduceTo> for SequencingToMinimizeWeightedTardiness { .ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< SequencingToMinimizeWeightedTardiness, - ILP, + ILP, >("summing task processing times") })?; let big_m = horizon; @@ -177,7 +177,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/shortestweightconstrainedpath_ilp.rs b/src/rules/shortestweightconstrainedpath_ilp.rs index c94aab980..1811f8a95 100644 --- a/src/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/rules/shortestweightconstrainedpath_ilp.rs @@ -6,7 +6,7 @@ //! bound constraint enforces the weight limit, and the objective minimizes //! total path length. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::ShortestWeightConstrainedPath; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -15,14 +15,14 @@ use crate::types::WeightElement; /// Result of reducing ShortestWeightConstrainedPath to ILP. /// -/// Variable layout (within `ILP`): +/// Variable layout (within `ILP`): /// - Arc variables: `a_{e,0}` and `a_{e,1}` for each undirected edge `e` /// (indices `0..2m`), bounded to {0, 1} /// - Order variables: `o_v` for each vertex `v` (indices `2m..2m+n`), /// bounded to `[0, n-1]` #[derive(Debug, Clone)] pub struct ReductionSWCPToILP { - target: ILP, + target: ILP, num_edges: usize, } @@ -34,9 +34,9 @@ impl ReductionSWCPToILP { impl ReductionResult for ReductionSWCPToILP { type Source = ShortestWeightConstrainedPath; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -66,7 +66,7 @@ impl ReductionResult for ReductionSWCPToILP { num_nonzeros = "(2 * num_edges + num_vertices) * (5 * num_edges + 4 * num_vertices + 2)", }, })] -impl ReduceTo> for ShortestWeightConstrainedPath { +impl ReduceTo> for ShortestWeightConstrainedPath { type Result = ReductionSWCPToILP; fn reduce_to(&self) -> Result { @@ -96,7 +96,7 @@ impl ReduceTo> for ShortestWeightConstrainedPath { let mut constraints = Vec::new(); - // --- Arc variables are binary within `ILP`: 0 <= a_{e,d} <= 1 --- + // --- Arc variables are binary within `ILP`: 0 <= a_{e,d} <= 1 --- for edge_idx in 0..num_edges { constraints.push(LinearConstraint::le( vec![(ReductionSWCPToILP::arc_var(edge_idx, 0), 1)], @@ -246,7 +246,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/strongconnectivityaugmentation_ilp.rs b/src/rules/strongconnectivityaugmentation_ilp.rs index 5fddf756f..bdae17fb3 100644 --- a/src/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/rules/strongconnectivityaugmentation_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from StrongConnectivityAugmentation to `ILP`. +//! Reduction from StrongConnectivityAugmentation to `ILP`. //! //! Select candidate arcs under the budget and certify strong connectivity by //! sending flow both from a root to every vertex and back again. @@ -11,15 +11,15 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionSCAToILP { - target: ILP, + target: ILP, num_candidates: usize, } impl ReductionResult for ReductionSCAToILP { type Source = StrongConnectivityAugmentation; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -49,7 +49,7 @@ impl crate::rules::AggregateReductionResult for ReductionSCAToILP {} num_constraints = "1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices", num_nonzeros = "(num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)) * (1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices)", })] -impl ReduceTo> for StrongConnectivityAugmentation { +impl ReduceTo> for StrongConnectivityAugmentation { type Result = ReductionSCAToILP; fn reduce_to(&self) -> Result { @@ -203,13 +203,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source) + crate::rules::ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let ilp_sol = crate::solvers::ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_sol).unwrap(); - crate::example_db::specs::rule_example_with_witness::<_, ILP>( + crate::example_db::specs::rule_example_with_witness::<_, ILP>( source, SolutionPair { source_config: serde_json::json!(extracted), diff --git a/src/rules/undirectedflowlowerbounds_ilp.rs b/src/rules/undirectedflowlowerbounds_ilp.rs index b8cf95beb..35cf0642b 100644 --- a/src/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/rules/undirectedflowlowerbounds_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from UndirectedFlowLowerBounds to `ILP`. +//! Reduction from UndirectedFlowLowerBounds to `ILP`. //! //! For each undirected edge e = {u,v} (indexed by e), we introduce: //! f_{uv} = 2*e (flow in u→v direction, ≥ 0) @@ -23,13 +23,13 @@ //! //! Size upper bound: 3*|E| variables, 5*|E| + |V| + 1 constraints. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::UndirectedFlowLowerBounds; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::Graph; -/// Result of reducing UndirectedFlowLowerBounds to `ILP`. +/// Result of reducing UndirectedFlowLowerBounds to `ILP`. /// /// Variable layout: /// - `f_{uv}` at 2*e (flow u→v on edge e) @@ -37,15 +37,15 @@ use crate::topology::Graph; /// - `z_e` at 2*|E| + e (orientation indicator: 1 = u→v direction) #[derive(Debug, Clone)] pub struct ReductionUFLBToILP { - target: ILP, + target: ILP, num_edges: usize, } impl ReductionResult for ReductionUFLBToILP { type Source = UndirectedFlowLowerBounds; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -83,7 +83,7 @@ impl crate::rules::AggregateReductionResult for ReductionUFLBToILP {} num_constraints = "5 * num_edges + num_vertices + 1", num_nonzeros = "(3 * num_edges) * (5 * num_edges + num_vertices + 1)", })] -impl ReduceTo> for UndirectedFlowLowerBounds { +impl ReduceTo> for UndirectedFlowLowerBounds { type Result = ReductionUFLBToILP; fn reduce_to(&self) -> Result { @@ -207,7 +207,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/rules/undirectedtwocommodityintegralflow_ilp.rs index 61a69fa4d..07da247ea 100644 --- a/src/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from UndirectedTwoCommodityIntegralFlow to `ILP`. +//! Reduction from UndirectedTwoCommodityIntegralFlow to `ILP`. //! //! For each undirected edge {u,v} (indexed by e), we introduce 4 flow variables: //! f1_{uv} = 4*e + 0 (commodity 1 flow u→v) @@ -13,7 +13,7 @@ //! For each edge e with capacity c_e, the joint capacity constraint is: //! max(f1_{uv}, f1_{vu}) + max(f2_{uv}, f2_{vu}) ≤ c_e //! -//! Since this is `ILP`, we use direction indicators d1_e, d2_e ∈ {0,1} to linearize: +//! Since this is `ILP`, we use direction indicators d1_e, d2_e ∈ {0,1} to linearize: //! f1_{uv} ≤ c_e * d1_e; f1_{vu} ≤ c_e * (1 - d1_e) //! f2_{uv} ≤ c_e * d2_e; f2_{vu} ≤ c_e * (1 - d2_e) //! f1_{uv} + f1_{vu} + f2_{uv} + f2_{vu} ≤ c_e (joint capacity) @@ -24,13 +24,13 @@ //! //! Constraints per edge (7 per edge) + flow conservation (2 per non-terminal vertex) + net flow (2) -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::UndirectedTwoCommodityIntegralFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::Graph; -/// Result of reducing UndirectedTwoCommodityIntegralFlow to `ILP`. +/// Result of reducing UndirectedTwoCommodityIntegralFlow to `ILP`. /// /// Variable layout: /// - `f1_{uv}` at 4*e + 0, `f1_{vu}` at 4*e + 1 (commodity 1 flows on edge e) @@ -38,15 +38,15 @@ use crate::topology::Graph; /// - `d1_e` at 4*|E| + 2*e, `d2_e` at 4*|E| + 2*e + 1 (direction indicators) #[derive(Debug, Clone)] pub struct ReductionU2CIFToILP { - target: ILP, + target: ILP, num_edges: usize, } impl ReductionResult for ReductionU2CIFToILP { type Source = UndirectedTwoCommodityIntegralFlow; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -78,7 +78,7 @@ impl crate::rules::AggregateReductionResult for ReductionU2CIFToILP {} num_nonzeros = "(6 * num_edges) * (7 * num_edges + num_conservation_constraints + 2)", }, })] -impl ReduceTo> for UndirectedTwoCommodityIntegralFlow { +impl ReduceTo> for UndirectedTwoCommodityIntegralFlow { type Result = ReductionU2CIFToILP; fn reduce_to(&self) -> Result { @@ -239,13 +239,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source) + .expect("reduction should succeed"); let solver = crate::solvers::ILPSolver::new(); let target_config = solver .solve(reduction.target_problem()) .expect("canonical example should be feasible"); let source_config = reduction.extract_solution(&target_config).unwrap(); - crate::example_db::specs::rule_example_with_witness::<_, ILP>( + crate::example_db::specs::rule_example_with_witness::<_, ILP>( source, SolutionPair { source_config: serde_json::to_value(source_config) diff --git a/src/solvers/ilp/adapter.rs b/src/solvers/ilp/adapter.rs index 2fa797aca..b01123aa0 100644 --- a/src/solvers/ilp/adapter.rs +++ b/src/solvers/ilp/adapter.rs @@ -4,7 +4,9 @@ //! type-erased dispatch, and reduction-chain extraction belong to the caller. //! Optimality and infeasibility follow HiGHS numerical tolerances, not exact proofs. -use crate::models::algebraic::{Comparison, ILPCoefficient, ObjectiveSense, VariableDomain, ILP}; +use crate::models::algebraic::{ + BoundsPolicy, Comparison, ILPCoefficient, ObjectiveSense, VariableDomain, ILP, +}; use crate::types::{i64_to_exact_f64, MAX_EXACT_F64_INTEGER}; use highs::{HighsModelStatus, HighsSolutionStatus, RowProblem, Sense}; @@ -45,10 +47,11 @@ impl HighsAdapter { pub(crate) fn new(time_limit: Option) -> Self { Self { time_limit } } - pub(crate) fn solve(&self, problem: &ILP) -> Result, ILPSolveError> + pub(crate) fn solve(&self, problem: &ILP) -> Result, ILPSolveError> where V: VariableDomain, C: BackendCoefficient, + B: BoundsPolicy, { if self .time_limit @@ -61,14 +64,15 @@ impl HighsAdapter { self.solve_with_objective(problem, problem.objective()) } - fn solve_with_objective( + fn solve_with_objective( &self, - problem: &ILP, + problem: &ILP, objective_terms: &[(usize, C)], ) -> Result, ILPSolveError> where V: VariableDomain, C: BackendCoefficient, + B: BoundsPolicy, { let n = problem.num_vars(); if n == 0 { @@ -171,8 +175,8 @@ impl HighsAdapter { } } -fn decode_and_validate( - problem: &ILP, +fn decode_and_validate( + problem: &ILP, values: impl IntoIterator, ) -> Result, ILPSolveError> { let result = values diff --git a/src/solvers/ilp/solver.rs b/src/solvers/ilp/solver.rs index 05893d30a..1e01e79fa 100644 --- a/src/solvers/ilp/solver.rs +++ b/src/solvers/ilp/solver.rs @@ -1,7 +1,7 @@ //! ILP solver implementation using HiGHS. use super::adapter::HighsAdapter; -use crate::models::algebraic::ILP; +use crate::models::algebraic::{Bounded, ILP}; use crate::solvers::registry::solver_capability_registry; use crate::solvers::ExactProblemKey; use crate::traits::Problem; @@ -120,6 +120,9 @@ impl ILPSolver { if let Some(ilp) = any.downcast_ref::>() { return HighsAdapter::new(self.time_limit).solve(ilp); } + if let Some(ilp) = any.downcast_ref::>() { + return HighsAdapter::new(self.time_limit).solve(ilp); + } if let Some(ilp) = any.downcast_ref::>() { return HighsAdapter::new(self.time_limit).solve(ilp); } diff --git a/src/solvers/pipelines.rs b/src/solvers/pipelines.rs index d10636f6a..14355a834 100644 --- a/src/solvers/pipelines.rs +++ b/src/solvers/pipelines.rs @@ -22,202 +22,206 @@ macro_rules! register_ilp_pipeline { } register_ilp_pipeline! { - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), + ("ILP", [("variable", "bool"), ("coefficient", "f64"), ("bounds", "general")]), } register_ilp_pipeline! { - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), + ("ILP", [("variable", "i64"), ("coefficient", "f64"), ("bounds", "general")]), +} + +register_ilp_pipeline! { + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("AcyclicPartition", [("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("BMF", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("BalancedCompleteBipartiteSubgraph", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("BicliqueCover", []), ("BMF", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("BiconnectivityAugmentation", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("BinPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("BottleneckTravelingSalesman", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("BoundedComponentSpanningForest", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("CapacityAssignment", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("CircuitSAT", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ClosestString", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("ClosestSubstring", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("Clustering", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ConsecutiveBlockMinimization", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ConsecutiveOnesMatrixAugmentation", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ConsecutiveOnesSubmatrix", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ConsistencyOfDatabaseFrequencyTables", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "One")]), ("DecisionMinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumSetCovering", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionOptimalLinearArrangement", [("graph", "SimpleGraph")]), ("OptimalLinearArrangement", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("DirectedHamiltonianPath", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DirectedTwoCommodityIntegralFlow", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("DisjointConnectingPaths", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("EnsembleComputation", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("EulerianPath", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("ExactCoverBy3Sets", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ExpectedRetrievalCost", []), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), + ("ILP", [("variable", "bool"), ("coefficient", "f64"), ("bounds", "general")]), } register_ilp_pipeline! { ("Factoring", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("FeasibleRegisterAssignment", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("FlowShopScheduling", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("GraphPartitioning", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("HamiltonianCircuit", [("graph", "SimpleGraph")]), ("DecisionLongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("HamiltonianPath", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("HighlyConnectedDeletion", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } // This exact variant also has a customized backend. Default dispatch selects the @@ -225,85 +229,85 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("RootedTreeArrangement", [("graph", "SimpleGraph")]), ("RootedTreeStorageAssignment", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("IntegralFlowBundles", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("IntegralFlowHomologousArcs", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("IntegralFlowWithMultipliers", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("IsomorphicSpanningTree", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("KClique", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("KColoring", [("graph", "SimpleGraph"), ("k", "KN")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("KColoring", [("graph", "SimpleGraph"), ("k", "K3")]), ("Clustering", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("KSatisfiability", [("k", "KN")]), ("Satisfiability", []), ("NAESatisfiability", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("Knapsack", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("LengthBoundedDisjointPaths", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("LongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("LongestCommonSubsequence", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("LongestPath", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MaximalIS", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("Maximum2Satisfiability", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { @@ -311,54 +315,54 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "One")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumCoKPlex", [("graph", "SimpleGraph"), ("k", "KN"), ("weight", "One")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumCoKPlex", [("graph", "SimpleGraph"), ("k", "KN"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumCommonEdgeSubgraph", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumContactMapOverlap", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumDomaticNumber", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumEdgeWeightedKClique", [("weight", "f64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), + ("ILP", [("variable", "bool"), ("coefficient", "f64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumEdgeWeightedKClique", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "One")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { @@ -367,13 +371,13 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "One")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { @@ -381,7 +385,7 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "One")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { @@ -389,7 +393,7 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "UnitDiskGraph"), ("weight", "i64")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { @@ -397,473 +401,473 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "UnitDiskGraph"), ("weight", "i64")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "UnitDiskGraph"), ("weight", "i64")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumLeafSpanningTree", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MaximumLikelihoodRanking", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumMatching", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumSetPacking", [("weight", "One")]), ("MaximumSetPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumSetPacking", [("weight", "f64")]), ("QUBO", [("weight", "f64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), + ("ILP", [("variable", "bool"), ("coefficient", "f64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumSetPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinMaxMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MinimumCapacitatedSpanningTree", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MinimumCoveringByCliques", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumCutIntoBoundedSets", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumDiscretePlanarInverseKinematics", []), ("QUBO", [("weight", "f64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), + ("ILP", [("variable", "bool"), ("coefficient", "f64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumEdgeCostFlow", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MinimumExternalMacroDataCompression", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumFaultDetectionTestSet", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumFeedbackArcSet", [("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MinimumFeedbackVertexSet", [("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MinimumGraphBandwidth", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MinimumHittingSet", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumInternalMacroDataCompression", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumMatrixCover", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumMaximalMatching", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumMetricDimension", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumMultiwayCut", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumSetCovering", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumTardinessSequencing", [("weight", "One")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumTardinessSequencing", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "One")]), ("MinimumHittingSet", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumSetCovering", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumWeightDecoding", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MixedChinesePostman", [("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MonochromaticTriangle", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MultipleCopyFileAllocation", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MultipleChoiceBranching", [("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MultiprocessorScheduling", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("NAESatisfiability", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("Numerical3DimensionalMatching", []), ("NumericalMatchingWithTargetSums", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("NumericalMatchingWithTargetSums", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("OpenShopScheduling", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("OptimalLinearArrangement", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("OptimumCommunicationSpanningTree", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("PaintShop", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("PartiallyOrderedKnapsack", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("Partition", []), ("MultiprocessorScheduling", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("PartitionIntoCliques", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("PartitionIntoPathsOfLength2", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("PartitionIntoTriangles", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("PathConstrainedNetworkFlow", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("PrecedenceConstrainedScheduling", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("PreemptiveScheduling", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("QUBO", [("weight", "f64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), + ("ILP", [("variable", "bool"), ("coefficient", "f64"), ("bounds", "general")]), } register_ilp_pipeline! { ("QUBO", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("QuadraticAssignment", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("RectilinearPictureCompression", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("RegisterSufficiency", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("ResourceConstrainedScheduling", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("RootedTreeStorageAssignment", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("RuralPostman", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("Satisfiability", []), ("NAESatisfiability", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SchedulingToMinimizeWeightedCompletionTime", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("SchedulingWithIndividualDeadlines", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SequencingToMinimizeMaximumCumulativeCost", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("SequencingToMinimizeTardyTaskWeight", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SequencingToMinimizeWeightedCompletionTime", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("SequencingToMinimizeWeightedTardiness", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("SequencingWithDeadlinesAndSetUpTimes", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SequencingWithReleaseTimesAndDeadlines", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SequencingWithinIntervals", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SetSplitting", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ShortestCommonSupersequence", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ShortestWeightConstrainedPath", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("SparseMatrixCompression", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SpinGlass", [("graph", "SimpleGraph"), ("weight", "f64")]), ("QUBO", [("weight", "f64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), + ("ILP", [("variable", "bool"), ("coefficient", "f64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SpinGlass", [("graph", "SimpleGraph"), ("weight", "i64")]), ("QUBO", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("StackerCrane", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SteinerTree", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("StringToStringCorrection", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("StrongConnectivityAugmentation", [("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SubgraphIsomorphism", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SumOfSquaresPartition", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ThreeDimensionalMatching", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ThreePartition", []), ("ResourceConstrainedScheduling", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("TravelingSalesman", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("UndirectedFlowLowerBounds", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("UndirectedTwoCommodityIntegralFlow", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("DecisionLongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMaximum2Satisfiability", []), ("Maximum2Satisfiability", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { @@ -871,66 +875,66 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "One")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMinimumCoveringByCliques", [("graph", "SimpleGraph")]), ("MinimumCoveringByCliques", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionOpenShopScheduling", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("DecisionQUBO", [("weight", "i64")]), ("QUBO", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionQuadraticAssignment", []), ("QuadraticAssignment", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionRuralPostman", [("graph", "SimpleGraph"), ("weight", "i64")]), ("RuralPostman", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("DecisionSequencingToMinimizeTardyTaskWeight", []), ("SequencingToMinimizeTardyTaskWeight", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionSpinGlass", [("graph", "SimpleGraph"), ("weight", "i64")]), ("SpinGlass", [("graph", "SimpleGraph"), ("weight", "i64")]), ("QUBO", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionStackerCrane", []), ("StackerCrane", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "One")]), ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "One")]), ("MinimumHittingSet", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } diff --git a/src/solvers/registry.rs b/src/solvers/registry.rs index c86d70ede..2fa82ac5f 100644 --- a/src/solvers/registry.rs +++ b/src/solvers/registry.rs @@ -50,12 +50,13 @@ impl ExactProblemKey { fn is_supported_ilp(&self) -> bool { self.name == "ILP" && matches!( - self.variant.get("variable").map(String::as_str), - Some("bool" | "i64") - ) - && matches!( - self.variant.get("coefficient").map(String::as_str), - Some("i64" | "f64") + ( + self.variant.get("variable").map(String::as_str), + self.variant.get("coefficient").map(String::as_str), + self.variant.get("bounds").map(String::as_str), + ), + (Some("bool" | "i64"), Some("i64" | "f64"), Some("general")) + | (Some("i64"), Some("i64"), Some("bounded")) ) } } diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index 168df6c82..a7fd51c9a 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -285,6 +285,7 @@ fn test_find_rule_example_integral_flow_bundles_to_ilp_contains_full_instances() variant: BTreeMap::from([ ("variable".to_string(), "i64".to_string()), ("coefficient".to_string(), "i64".to_string()), + ("bounds".to_string(), "bounded".to_string()), ]), }; @@ -315,6 +316,7 @@ fn test_find_rule_example_threedimensionalmatching_to_ilp_contains_full_instance variant: BTreeMap::from([ ("variable".to_string(), "bool".to_string()), ("coefficient".to_string(), "i64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), }; diff --git a/src/unit_tests/models/algebraic/ilp.rs b/src/unit_tests/models/algebraic/ilp.rs index 64bdc8143..a7539acec 100644 --- a/src/unit_tests/models/algebraic/ilp.rs +++ b/src/unit_tests/models/algebraic/ilp.rs @@ -3,6 +3,37 @@ use crate::solvers::{ILPSolveError, ILPSolver}; use crate::traits::Problem; use crate::types::Extremum; +#[test] +fn bounded_integer_ilp_loading_requires_finite_domains() { + assert!(ILP::::new(1, vec![], vec![], ObjectiveSense::Minimize).is_ok()); + assert!(ILP::::new(1, vec![], vec![], ObjectiveSense::Minimize).is_err()); + assert_eq!(ILP::::empty().num_vars(), 0); + let variant = crate::rules::ReductionGraph::variant_to_map(&[ + ("variable", "i64"), + ("coefficient", "i64"), + ("bounds", "bounded"), + ]); + let instance = serde_json::json!({ + "variables": [{"lower_bound": -2, "upper_bound": 3}], + "constraints": [], "objective": [[0, 1]], "sense": "Maximize" + }); + let loaded = crate::registry::load_dyn("ILP", &variant, instance.clone()) + .expect("bounded integer ILP must be registered"); + assert_eq!(loaded.serialize_json(), instance); + for (lower, upper) in [(None, Some(3)), (Some(-2), None), (None, None)] { + let mut invalid = instance.clone(); + invalid["variables"] = serde_json::json!([{"lower_bound": lower, "upper_bound": upper}]); + assert!(crate::registry::load_dyn("ILP", &variant, invalid).is_err()); + assert!(ILP::::with_variables( + vec![IntegerVariable::new(lower, upper).unwrap()], + vec![], + vec![], + ObjectiveSense::Minimize, + ) + .is_err()); + } +} + fn binary_ilp( num_vars: usize, constraints: Vec, @@ -16,7 +47,11 @@ fn binary_ilp( fn ilp_variant_identifies_variable_domain() { assert_eq!( as Problem>::variant(), - vec![("variable", "bool"), ("coefficient", "i64")] + vec![ + ("variable", "bool"), + ("coefficient", "i64"), + ("bounds", "general") + ] ); } @@ -24,7 +59,11 @@ fn ilp_variant_identifies_variable_domain() { fn ilp_variant_identifies_float_coefficients() { assert_eq!( as Problem>::variant(), - vec![("variable", "bool"), ("coefficient", "f64")] + vec![ + ("variable", "bool"), + ("coefficient", "f64"), + ("bounds", "general") + ] ); } diff --git a/src/unit_tests/parameter_formula_validation.rs b/src/unit_tests/parameter_formula_validation.rs index 183c2111f..db078d217 100644 --- a/src/unit_tests/parameter_formula_validation.rs +++ b/src/unit_tests/parameter_formula_validation.rs @@ -157,8 +157,8 @@ fn source_for( } #[test] -fn integer_ilp_reductions_support_binary_encoding() { - use crate::models::algebraic::ILP; +fn bounded_ilp_reductions_support_binary_encoding() { + use crate::models::algebraic::{Bounded, ILP}; use crate::rules::ReduceTo; use crate::traits::Problem; @@ -166,7 +166,9 @@ fn integer_ilp_reductions_support_binary_encoding() { let mut failures = Vec::new(); let mut checked = 0; for entry in crate::rules::registry::reduction_entries() { - if entry.target_name != "ILP" || entry.target_variant() != ILP::::variant() { + if entry.target_name != "ILP" + || entry.target_variant() != ILP::::variant() + { continue; } let result = (|| { @@ -175,7 +177,7 @@ fn integer_ilp_reductions_support_binary_encoding() { entry.reduce_fn.unwrap()(source.as_any()).map_err(|error| error.to_string())?; let integer = reduced .target_problem_any() - .downcast_ref::>() + .downcast_ref::>() .unwrap(); ReduceTo::>::reduce_to(integer).map_err(|error| error.to_string())?; Ok::<_, String>(()) diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index 0370dbb4b..d059532f8 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -11,6 +11,31 @@ use crate::types::ProblemParameters; use crate::variant::{K3, KN}; use std::collections::BTreeMap; +#[test] +fn integer_ilp_graph_requires_bounded_domains_for_binary_encoding() { + let graph = ReductionGraph::new(); + let general = ReductionGraph::variant_to_map(&ILP::::variant()); + let binary = ReductionGraph::variant_to_map(&ILP::::variant()); + assert!(graph + .find_all_paths("ILP", &general, "ILP", &binary) + .is_empty()); + let bounded = ReductionGraph::variant_to_map(&[ + ("variable", "i64"), + ("coefficient", "i64"), + ("bounds", "bounded"), + ]); + for (source, target) in [ + (&bounded, &binary), + (&bounded, &general), + (&binary, &bounded), + ] { + assert!(graph + .find_all_paths("ILP", source, "ILP", target) + .iter() + .any(|path| path.len() == 1)); + } +} + #[test] fn exact_transform_evaluates_without_path_ranking() { let graph = ReductionGraph::new(); diff --git a/src/unit_tests/rules/acyclicpartition_ilp.rs b/src/unit_tests/rules/acyclicpartition_ilp.rs index 5574da900..c09f19a9d 100644 --- a/src/unit_tests/rules/acyclicpartition_ilp.rs +++ b/src/unit_tests/rules/acyclicpartition_ilp.rs @@ -21,7 +21,7 @@ fn small_instance() -> AcyclicPartition { fn test_acyclicpartition_to_ilp_closed_loop() { let source = small_instance(); let reduction: ReductionAcyclicPartitionToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Solve source with brute force @@ -44,7 +44,7 @@ fn test_acyclicpartition_to_ilp_closed_loop() { fn test_reduction_num_vars() { let source = small_instance(); let reduction: ReductionAcyclicPartitionToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 35); assert_eq!(ilp.num_constraints(), 75); @@ -69,12 +69,12 @@ fn signed_partition_weights_and_costs_are_checked_after_summing() { ), ] { assert!(source.evaluate(&witness).unwrap().0); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } let empty = AcyclicPartition::new(DirectedGraph::new(0, vec![]), vec![], vec![], 0, -1); assert!(!empty.evaluate(&vec![]).unwrap().0); - let reduction = ReduceTo::>::reduce_to(&empty).unwrap(); + let reduction = ReduceTo::>::reduce_to(&empty).unwrap(); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } @@ -82,7 +82,7 @@ fn signed_partition_weights_and_costs_are_checked_after_summing() { fn test_extract_solution() { let source = small_instance(); let reduction: ReductionAcyclicPartitionToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("ILP should be solvable"); @@ -105,7 +105,7 @@ fn test_infeasible_instance() { 0, ); let reduction: ReductionAcyclicPartitionToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); assert!(solver.solve(ilp).is_err()); @@ -115,7 +115,7 @@ fn test_infeasible_instance() { fn test_acyclicpartition_to_ilp_bf_vs_ilp() { let source = small_instance(); let reduction: ReductionAcyclicPartitionToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } @@ -129,7 +129,7 @@ fn test_acyclicpartition_to_ilp_regression_direct_topological_labels() { 10, ); let reduction: ReductionAcyclicPartitionToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("the feasible source instance must yield a feasible ILP"); diff --git a/src/unit_tests/rules/aggregate_contracts.rs b/src/unit_tests/rules/aggregate_contracts.rs index 7c5d1723b..08c82ea67 100644 --- a/src/unit_tests/rules/aggregate_contracts.rs +++ b/src/unit_tests/rules/aggregate_contracts.rs @@ -1,3 +1,4 @@ +use crate::models::algebraic::Bounded; use crate::models::algebraic::MinimumWeightDecoding; use crate::models::formula::{CNFClause, KSatisfiability}; use crate::models::graph::{ @@ -141,7 +142,7 @@ fn empty_tree_storage_still_obeys_the_budget() { for n in 0..=2 { for bound in [-1, 0] { let source = RootedTreeStorageAssignment::new(n, vec![], bound); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let expected = bound >= 0; assert_eq!( BruteForce::new().solve(&source).unwrap().is_some(), diff --git a/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs b/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs index 249dabc98..4f39a3578 100644 --- a/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs +++ b/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs @@ -19,7 +19,7 @@ fn small_instance() -> BiconnectivityAugmentation { fn test_biconnectivityaugmentation_to_ilp_closed_loop() { let source = small_instance(); let reduction: ReductionBiconnAugToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Solve source with brute force @@ -42,7 +42,7 @@ fn test_biconnectivityaugmentation_to_ilp_closed_loop() { fn test_extract_solution() { let source = small_instance(); let reduction: ReductionBiconnAugToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("ILP should be solvable"); @@ -55,7 +55,7 @@ fn test_extract_solution() { fn test_trivial_single_vertex() { let source = BiconnectivityAugmentation::new(SimpleGraph::new(1, vec![]), vec![], 0); let reduction: ReductionBiconnAugToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("trivial ILP should be solvable"); @@ -72,7 +72,7 @@ fn test_already_biconnected() { 0, ); let reduction: ReductionBiconnAugToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver @@ -86,7 +86,7 @@ fn test_already_biconnected() { fn test_biconnectivityaugmentation_to_ilp_bf_vs_ilp() { let source = small_instance(); let reduction: ReductionBiconnAugToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } @@ -105,7 +105,7 @@ fn test_biconnectivityaugmentation_to_ilp_all_two_vertex_instances() { budget, ); let reduction: ReductionBiconnAugToILP = - ReduceTo::>::reduce_to(&source).unwrap(); + ReduceTo::>::reduce_to(&source).unwrap(); let expected = BruteForce::new().solve(&source).unwrap().is_some(); match ILPSolver::new().solve(reduction.target_problem()) { Ok(z) => { @@ -132,7 +132,7 @@ fn test_biconnectivityaugmentation_to_ilp_empty_negative_budget() { let source = BiconnectivityAugmentation::<_, i64>::new(SimpleGraph::empty(n), vec![], budget); let reduction: ReductionBiconnAugToILP = - ReduceTo::>::reduce_to(&source).unwrap(); + ReduceTo::>::reduce_to(&source).unwrap(); assert_eq!( reduction .target_problem() @@ -151,7 +151,7 @@ fn test_biconnectivityaugmentation_to_ilp_empty_negative_budget() { 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 reduction: ReductionBiconnAugToILP = ReduceTo::>::reduce_to(&source).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(); assert!( diff --git a/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs b/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs index 45383e2f4..967746104 100644 --- a/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs @@ -14,7 +14,7 @@ fn k4_btsp() -> BottleneckTravelingSalesman { fn test_reduction_creates_valid_ilp() { let problem = k4_btsp(); let reduction: ReductionBTSPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // n=4, m=6: 16 position bits, 48 edge-use bits, 6 maximum selectors. assert_eq!(ilp.num_vars(), 70); @@ -29,7 +29,7 @@ fn test_bottlenecktravelingsalesman_to_ilp_closed_loop() { let bf_value = problem.evaluate(&bf_solution).unwrap(); let reduction: ReductionBTSPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -59,7 +59,7 @@ fn test_bottlenecktravelingsalesman_to_ilp_c4() { let bf_value = problem.evaluate(&bf_solution).unwrap(); let reduction: ReductionBTSPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -75,7 +75,7 @@ fn test_bottlenecktravelingsalesman_to_ilp_c4() { fn test_solution_extraction() { let problem = k4_btsp(); let reduction: ReductionBTSPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -93,7 +93,7 @@ fn test_no_hamiltonian_cycle_infeasible() { vec![1, 1, 1], ); let reduction: ReductionBTSPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(reduction.target_problem()); assert!( @@ -106,7 +106,7 @@ fn test_no_hamiltonian_cycle_infeasible() { fn test_bottlenecktravelingsalesman_to_ilp_bf_vs_ilp() { let problem = k4_btsp(); let reduction: ReductionBTSPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } @@ -169,7 +169,7 @@ fn test_bottleneck_ilp_signed_full_range_and_native_cycles() { ), ] { let source = BottleneckTravelingSalesman::new(SimpleGraph::new(n, edges), weights); - let result = ReduceTo::>::reduce_to(&source).unwrap(); + let result = ReduceTo::>::reduce_to(&source).unwrap(); let witness = tour_witness(&source, &tour, &edge_order); let extracted = result.extract_solution(&witness).unwrap(); let expected = source.evaluate(&extracted).unwrap().unwrap(); @@ -192,7 +192,7 @@ fn test_bottleneck_ilp_signed_full_range_and_native_cycles() { #[test] fn test_bottleneck_ilp_maximum_must_be_used_and_dominate() { let source = k4_btsp(); - let result = ReduceTo::>::reduce_to(&source).unwrap(); + let result = ReduceTo::>::reduce_to(&source).unwrap(); 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); @@ -208,7 +208,7 @@ fn test_bottleneck_ilp_maximum_must_be_used_and_dominate() { 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 result = ReduceTo::>::reduce_to(&source).unwrap(); + let result = ReduceTo::>::reduce_to(&source).unwrap(); assert!(matches!( ILPSolver::new().solve(result.target_problem()), Err(crate::solvers::ILPSolveError::Infeasible) diff --git a/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs b/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs index 039d7f2c3..c2cbea23c 100644 --- a/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::models::graph::BoundedComponentSpanningForest; use crate::rules::ReduceTo; @@ -20,7 +21,7 @@ fn small_instance() -> BoundedComponentSpanningForest { fn test_boundedcomponentspanningforest_to_ilp_closed_loop() { let source = small_instance(); let reduction: ReductionBCSFToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Solve source with brute force @@ -43,7 +44,7 @@ fn test_boundedcomponentspanningforest_to_ilp_closed_loop() { fn test_extract_solution() { let source = small_instance(); let reduction: ReductionBCSFToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("ILP should be solvable"); @@ -62,7 +63,7 @@ fn test_single_component() { 3, ); let reduction: ReductionBCSFToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver @@ -82,7 +83,7 @@ fn test_infeasible_instance() { 5, ); let reduction: ReductionBCSFToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); assert!(solver.solve(ilp).is_err()); @@ -92,6 +93,6 @@ fn test_infeasible_instance() { fn test_boundedcomponentspanningforest_to_ilp_bf_vs_ilp() { let source = small_instance(); let reduction: ReductionBCSFToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + 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/closeststring_ilp.rs b/src/unit_tests/rules/closeststring_ilp.rs index 5714565a7..487b7f68e 100644 --- a/src/unit_tests/rules/closeststring_ilp.rs +++ b/src/unit_tests/rules/closeststring_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::misc::ClosestString; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::solvers::{BruteForce, ILPSolver}; @@ -19,7 +19,8 @@ fn issue_instance() -> ClosestString { #[test] fn test_closeststring_to_ilp_structure() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // q = 2, m = 3 -> 2*3 + 1 = 7 variables. @@ -51,7 +52,8 @@ fn test_closeststring_to_ilp_structure() { #[test] fn test_closeststring_to_ilp_closed_loop() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let bf_value_solution = BruteForce::new().solve(&source).unwrap().unwrap(); @@ -72,7 +74,8 @@ fn test_closeststring_to_ilp_closed_loop() { #[test] fn test_closeststring_to_ilp_bf_vs_ilp() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); assert_bf_vs_ilp(&source, &reduction); } @@ -81,7 +84,8 @@ fn test_closeststring_to_ilp_extract_known_center() { // Build the binary encoding of the center 000 by hand: // x_{0,0}=x_{1,0}=x_{2,0}=1, others 0, R = 2. let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let mut target_solution = vec![0_i64; reduction.target_problem().num_vars()]; target_solution[0] = 1; // x_{0,0} @@ -97,7 +101,8 @@ 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 reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target_solution = vec![0; reduction.target_problem().num_vars()]; assert_eq!( @@ -114,7 +119,8 @@ 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 reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // q * m + 1 = 3 * 2 + 1 = 7 variables; m + n = 2 + 3 = 5 constraints. @@ -130,7 +136,8 @@ fn test_closeststring_to_ilp_single_string_zero_radius() { // 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 reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) diff --git a/src/unit_tests/rules/closestsubstring_ilp.rs b/src/unit_tests/rules/closestsubstring_ilp.rs index bf3a9fed5..ed20f8b40 100644 --- a/src/unit_tests/rules/closestsubstring_ilp.rs +++ b/src/unit_tests/rules/closestsubstring_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::misc::ClosestSubstring; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::solvers::{BruteForce, ILPSolver}; @@ -24,7 +24,8 @@ fn issue_instance() -> ClosestSubstring { #[test] fn test_closestsubstring_to_ilp_structure() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // q = 2, ell = 3, total windows W = 3 + 3 + 3 = 9. @@ -74,7 +75,8 @@ fn test_closestsubstring_to_ilp_structure() { #[test] fn test_closestsubstring_to_ilp_rejects_missing_one_hot_symbol() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target_solution = vec![0; reduction.target_problem().num_vars()]; assert_eq!( @@ -89,7 +91,8 @@ fn test_closestsubstring_to_ilp_rejects_missing_one_hot_symbol() { #[test] fn test_closestsubstring_to_ilp_closed_loop() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let bf_value_solution = BruteForce::new().solve(&source).unwrap().unwrap(); @@ -112,7 +115,8 @@ fn test_closestsubstring_to_ilp_closed_loop() { #[test] fn test_closestsubstring_to_ilp_bf_vs_ilp() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); assert_bf_vs_ilp(&source, &reduction); } @@ -126,7 +130,8 @@ fn test_closestsubstring_to_ilp_zero_radius_when_common_substring_exists() { 3, ) .unwrap(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) @@ -144,7 +149,8 @@ fn test_closestsubstring_to_ilp_ternary_alphabet() { // enough to cross-check via the closed loop. let source = ClosestSubstring::new(3, vec![vec![0, 1, 2], vec![1, 2, 0], vec![2, 0, 1]], 2).unwrap(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // q*ell + W + 1 with W = 2 + 2 + 2 = 6: num_vars = 6 + 6 + 1 = 13. @@ -162,7 +168,8 @@ fn test_closestsubstring_to_ilp_extract_known_solution() { // y_{1,0}=y_{2,1}=y_{3,0}=1, R = 1. Then verify the extracted source // config matches and gives radius 1. let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let mut target_solution = vec![0_i64; ilp.num_vars()]; diff --git a/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs b/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs index 263c6947e..644db2151 100644 --- a/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -16,7 +16,7 @@ fn sink_self_loop_cannot_supply_commodity_flow() { 1, 0, ); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); assert!(!source.evaluate(&vec![1, 0]).unwrap().0); assert!(reduction .target_problem() @@ -73,7 +73,7 @@ fn infeasible_instance() -> DirectedTwoCommodityIntegralFlow { fn test_directedtwocommodityintegralflow_to_ilp_structure() { let problem = feasible_instance(); let reduction: ReductionD2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 8 arcs → 2*8 = 16 variables @@ -103,7 +103,7 @@ fn test_directedtwocommodityintegralflow_to_ilp_closed_loop() { ); let reduction: ReductionD2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -119,7 +119,7 @@ fn test_directedtwocommodityintegralflow_to_ilp_closed_loop() { fn test_directedtwocommodityintegralflow_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionD2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "infeasible flow instance should produce infeasible ILP" @@ -132,7 +132,7 @@ fn test_directedtwocommodityintegralflow_to_ilp_disallows_using_other_commodity_ let problem = DirectedTwoCommodityIntegralFlow::new(graph, vec![1, 1], 0, 1, 2, 3, 1, 0); let reduction: ReductionD2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "commodity 1 must conserve flow at commodity 2's source in the ILP reduction" @@ -143,7 +143,7 @@ fn test_directedtwocommodityintegralflow_to_ilp_disallows_using_other_commodity_ fn test_directedtwocommodityintegralflow_to_ilp_extract_solution() { let problem = feasible_instance(); let reduction: ReductionD2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // f1 routes via (0,2),(2,4): arcs 0,4 = 1; rest 0 for commodity 1 // f2 routes via (1,3),(3,5): arcs 3,7 = 1; rest 0 for commodity 2 @@ -165,7 +165,7 @@ fn test_directedtwocommodityintegralflow_to_ilp_extract_solution() { fn test_directedtwocommodityintegralflow_to_ilp_bf_vs_ilp() { let problem = feasible_instance(); let reduction: ReductionD2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } @@ -183,7 +183,7 @@ fn test_directedtwocommodityintegralflow_to_ilp_preserves_large_exact_capacity() 1, ); - let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let capacity_constraint = &reduction.target_problem().constraints()[0]; assert_eq!(capacity_constraint.terms(), vec![(0, 1), (1, 1)]); assert_eq!(capacity_constraint.rhs(), capacity); diff --git a/src/unit_tests/rules/ensemblecomputation_ilp.rs b/src/unit_tests/rules/ensemblecomputation_ilp.rs index 3be3c0aaa..09b185376 100644 --- a/src/unit_tests/rules/ensemblecomputation_ilp.rs +++ b/src/unit_tests/rules/ensemblecomputation_ilp.rs @@ -12,7 +12,7 @@ fn feasible_instance() -> EnsembleComputation { #[test] fn test_ensemblecomputation_to_ilp_structure() { - let reduction = ReduceTo::>::reduce_to(&feasible_instance()).unwrap(); + let reduction = ReduceTo::>::reduce_to(&feasible_instance()).unwrap(); assert_eq!(reduction.target_problem().num_vars(), 75); assert_eq!(reduction.target_problem().constraints().len(), 154); } @@ -20,7 +20,7 @@ fn test_ensemblecomputation_to_ilp_structure() { #[test] fn test_ensemblecomputation_to_ilp_closed_loop() { let source = feasible_instance(); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target_solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); let extracted = reduction.extract_solution(&target_solution).unwrap(); assert_eq!(source.evaluate(&extracted).unwrap(), Min(Some(3))); @@ -29,21 +29,21 @@ fn test_ensemblecomputation_to_ilp_closed_loop() { #[test] 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(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } #[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(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } #[test] fn test_ensemblecomputation_to_ilp_empty_family() { let source = EnsembleComputation::new(1, vec![], 2); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target_solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); let extracted = reduction.extract_solution(&target_solution).unwrap(); assert_eq!(source.evaluate(&extracted).unwrap(), Min(Some(0))); diff --git a/src/unit_tests/rules/eulerianpath_ilp.rs b/src/unit_tests/rules/eulerianpath_ilp.rs index 7d9f5c3c4..1a3abc688 100644 --- a/src/unit_tests/rules/eulerianpath_ilp.rs +++ b/src/unit_tests/rules/eulerianpath_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::graph::EulerianPath; use crate::solvers::ILPSolver; use crate::topology::DirectedGraph; @@ -15,7 +15,8 @@ fn issue_instance() -> EulerianPath { #[test] fn test_eulerianpath_to_ilp_issue_structure() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // m = 4 arcs. Compatible pairs: head(a) = tail(b), a != b: @@ -44,7 +45,8 @@ fn test_eulerianpath_to_ilp_issue_structure() { fn test_eulerianpath_to_ilp_empty_instance() { // m = 0: empty ILP, vacuously feasible. let source = EulerianPath::new(DirectedGraph::empty(3)); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 0); assert_eq!(ilp.constraints().len(), 0); @@ -62,7 +64,8 @@ fn test_eulerianpath_to_ilp_closed_loop() { // Solve the ILP on the canonical instance and verify the extracted ordering // is a valid directed Eulerian trail in the source. let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible for a YES instance"); @@ -83,7 +86,8 @@ fn test_eulerianpath_to_ilp_infeasible_no_instance() { // 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 reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + 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()); @@ -99,7 +103,8 @@ 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 reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) diff --git a/src/unit_tests/rules/factoring_ilp.rs b/src/unit_tests/rules/factoring_ilp.rs index a7ec16aa1..1a1eb5c55 100644 --- a/src/unit_tests/rules/factoring_ilp.rs +++ b/src/unit_tests/rules/factoring_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::solvers::{BruteForce, ILPSolver}; use num_bigint::BigUint; @@ -7,7 +8,7 @@ fn test_reduction_creates_valid_ilp() { // Factor 6 with 2-bit factors let problem = Factoring::with_factor_bits(6, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Check variable count: m + n + m*n + (m+n) = 2 + 2 + 4 + 4 = 12 @@ -23,7 +24,7 @@ fn test_reduction_creates_valid_ilp() { fn test_variable_layout() { let problem = Factoring::with_factor_bits(6, 2, 3); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // p variables: [0, 1] assert_eq!(reduction.p_var(0), 0); @@ -49,7 +50,7 @@ fn test_factor_6() { // 6 = 2 × 3 or 3 × 2 let problem = Factoring::with_factor_bits(6, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -72,7 +73,7 @@ fn test_factor_15() { // 2. Reduce to ILP let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 3. Solve ILP @@ -93,7 +94,7 @@ fn test_factor_35() { // 35 = 5 × 7 or 7 × 5 let problem = Factoring::with_factor_bits(35, 3, 3); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -111,7 +112,7 @@ fn test_factor_one() { // 1 = 1 × 1 let problem = Factoring::with_factor_bits(1, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -129,7 +130,7 @@ fn test_factor_prime() { // 7 is prime: 7 = 1 × 7 or 7 × 1 let problem = Factoring::with_factor_bits(7, 3, 3); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -147,7 +148,7 @@ fn test_factor_square() { // 9 = 3 × 3 let problem = Factoring::with_factor_bits(9, 3, 3); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -165,7 +166,7 @@ fn test_infeasible_target_too_large() { // Target 100 with 2-bit factors (max product is 3 × 3 = 9) let problem = Factoring::with_factor_bits(100, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -178,7 +179,7 @@ fn test_infeasible_target_too_large() { fn test_factoring_to_ilp_closed_loop() { let problem = Factoring::with_factor_bits(6, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Get ILP solution @@ -207,7 +208,7 @@ fn test_factoring_to_ilp_closed_loop() { fn test_solution_extraction() { let problem = Factoring::with_factor_bits(6, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Manually construct ILP solution for 2 × 3 = 6 // p = 2 = binary 10 -> p_0=0, p_1=1 @@ -230,7 +231,7 @@ fn test_solution_extraction() { fn test_target_ilp_structure() { let problem = Factoring::with_factor_bits(12, 3, 4); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // num_vars = 3 + 4 + 12 + 7 = 26 @@ -245,7 +246,7 @@ fn test_integer_ilp_pipeline_solution() { let problem = Factoring::with_factor_bits(6, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); @@ -259,7 +260,7 @@ fn test_asymmetric_bit_widths() { // 12 = 3 × 4 or 4 × 3 or 2 × 6 or 6 × 2 or 1 × 12 or 12 × 1 let problem = Factoring::with_factor_bits(12, 2, 4); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -276,7 +277,7 @@ fn test_asymmetric_bit_widths() { 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(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } @@ -287,7 +288,8 @@ fn test_constraint_count_formula() { for (m, n) in [(2, 2), (3, 3), (2, 4), (3, 4)] { let problem = Factoring::with_factor_bits(1, m, n); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem) + .expect("reduction should succeed"); let ilp = reduction.target_problem(); let expected = 3 * m * n + 4 * m + 4 * n + 1; @@ -307,7 +309,8 @@ fn test_variable_count_formula() { for (m, n) in [(2, 2), (3, 3), (2, 4), (3, 4)] { let problem = Factoring::with_factor_bits(1, m, n); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem) + .expect("reduction should succeed"); let ilp = reduction.target_problem(); let expected = m + n + m * n + (m + n); @@ -325,6 +328,6 @@ fn test_variable_count_formula() { fn test_factoring_to_ilp_bf_vs_ilp() { let problem = Factoring::with_factor_bits(6, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + 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/feasibleregisterassignment_ilp.rs b/src/unit_tests/rules/feasibleregisterassignment_ilp.rs index 6b18e1e2a..3f296cc80 100644 --- a/src/unit_tests/rules/feasibleregisterassignment_ilp.rs +++ b/src/unit_tests/rules/feasibleregisterassignment_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::solvers::ILPSolver; use crate::traits::Problem; use crate::types::Or; @@ -10,7 +11,8 @@ fn feasible_example() -> FeasibleRegisterAssignment { #[test] fn test_feasible_register_assignment_to_ilp_structure() { let source = feasible_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 14); @@ -22,7 +24,8 @@ fn test_feasible_register_assignment_to_ilp_structure() { #[test] fn test_feasible_register_assignment_to_ilp_closed_loop() { let source = feasible_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) @@ -38,7 +41,8 @@ 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 reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), @@ -49,6 +53,7 @@ fn test_feasible_register_assignment_to_ilp_infeasible() { #[test] fn test_feasible_register_assignment_to_ilp_bf_vs_ilp() { let source = feasible_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + 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/flowshopscheduling_ilp.rs b/src/unit_tests/rules/flowshopscheduling_ilp.rs index 7b8011ec3..580b2e6a8 100644 --- a/src/unit_tests/rules/flowshopscheduling_ilp.rs +++ b/src/unit_tests/rules/flowshopscheduling_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -7,7 +8,7 @@ use crate::types::Or; #[test] fn zero_duration_jobs_preserve_the_common_machine_order() { let source = FlowShopScheduling::new(2, vec![vec![3, 0], vec![1, 10]], 11); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); // Job 1 precedes job 0, but both finish on machine 1 at time 11. let assignment = vec![0, 4, 11, 1, 11]; assert!(reduction @@ -21,7 +22,7 @@ fn zero_duration_jobs_preserve_the_common_machine_order() { assert_eq!(source.evaluate(&decoded).unwrap(), Or(true)); let no_machines = FlowShopScheduling::new(0, vec![vec![], vec![]], 0); - let reduction = ReduceTo::>::reduce_to(&no_machines).unwrap(); + let reduction = ReduceTo::>::reduce_to(&no_machines).unwrap(); crate::rules::test_helpers::assert_bf_vs_ilp(&no_machines, &reduction); } @@ -29,7 +30,8 @@ fn zero_duration_jobs_preserve_the_common_machine_order() { 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 reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf = BruteForce::new(); let bf_witness = bf @@ -53,7 +55,8 @@ fn test_flowshopscheduling_to_ilp_closed_loop() { 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 reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "infeasible FSS should produce infeasible ILP" @@ -64,7 +67,8 @@ fn test_flowshopscheduling_to_ilp_infeasible() { fn test_flowshopscheduling_to_ilp_single_job() { // 2 machines, 1 job, deadline 10 let problem = FlowShopScheduling::new(2, vec![vec![3, 4]], 10); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("single-job ILP should be solvable"); @@ -75,7 +79,8 @@ 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 reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf = BruteForce::new(); let bf_witness = bf.solve(&problem).unwrap().expect("should be feasible"); diff --git a/src/unit_tests/rules/ilp_bool_ilp_i64.rs b/src/unit_tests/rules/ilp_bool_ilp_i64.rs index 2c31e6393..6394c2e30 100644 --- a/src/unit_tests/rules/ilp_bool_ilp_i64.rs +++ b/src/unit_tests/rules/ilp_bool_ilp_i64.rs @@ -1,4 +1,4 @@ -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, LinearConstraint, ObjectiveSense, ILP}; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::solvers::ILPSolver; use crate::traits::Problem; @@ -20,7 +20,8 @@ fn test_ilp_bool_to_ilp_i64_closed_loop() { let source_best = ILPSolver::new().solve(&source).unwrap(); let source_obj = source.evaluate(&source_best).unwrap(); - let result = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let result = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = result.target_problem(); // Target should have same number of variables @@ -37,7 +38,8 @@ fn test_ilp_bool_to_ilp_i64_closed_loop() { #[test] fn test_ilp_bool_to_ilp_i64_empty() { let source = ILP::::empty(); - let result = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let result = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = result.target_problem(); assert_eq!(target.num_vars(), 0); assert!(target.constraints().is_empty()); @@ -58,7 +60,8 @@ fn test_ilp_bool_to_ilp_i64_preserves_constraints() { ) .unwrap(); - let result = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let result = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = result.target_problem(); assert_eq!(target.constraints(), source.constraints()); diff --git a/src/unit_tests/rules/ilp_bounded_ilp.rs b/src/unit_tests/rules/ilp_bounded_ilp.rs new file mode 100644 index 000000000..62aa97aee --- /dev/null +++ b/src/unit_tests/rules/ilp_bounded_ilp.rs @@ -0,0 +1,40 @@ +use super::*; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense}; +use crate::rules::AggregateReductionResult; +use crate::solvers::ILPSolver; +use crate::traits::Problem; +use crate::types::Extremum; + +#[test] +fn bounded_ilp_embedding_preserves_domains_optima_and_values() { + for (sense, expected) in [ + (ObjectiveSense::Minimize, -6), + (ObjectiveSense::Maximize, 3), + ] { + let source = ILP::::with_variables( + vec![IntegerVariable::new(Some(-2), Some(4)).unwrap()], + vec![LinearConstraint::le(vec![(0, 1)], 1)], + vec![(0, 3)], + sense, + ) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = ReductionResult::target_problem(&reduction); + assert_eq!(target.variables(), source.variables()); + assert_eq!(target.parameters(), source.parameters()); + let solution = ILPSolver::new().solve(target).unwrap(); + let recovered = reduction.extract_solution(&solution).unwrap(); + let value = match sense { + ObjectiveSense::Minimize => Extremum::minimize(Some(expected)), + ObjectiveSense::Maximize => Extremum::maximize(Some(expected)), + }; + assert_eq!(source.evaluate(&recovered).unwrap(), value); + assert_eq!( + reduction.extract_value(target.evaluate(&solution).unwrap()), + value + ); + assert!(reduction.extract_solution(&vec![-3]).is_err()); + assert!(reduction.extract_solution(&vec![2]).is_err()); + assert!(reduction.extract_solution(&vec![]).is_err()); + } +} diff --git a/src/unit_tests/rules/ilp_i64_ilp_bool.rs b/src/unit_tests/rules/ilp_i64_ilp_bool.rs index 75be4dc93..94aec190b 100644 --- a/src/unit_tests/rules/ilp_i64_ilp_bool.rs +++ b/src/unit_tests/rules/ilp_i64_ilp_bool.rs @@ -1,4 +1,4 @@ -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::solvers::ILPSolver; @@ -7,7 +7,7 @@ fn integer_ilp( constraints: Vec, objective: Vec<(usize, i64)>, sense: ObjectiveSense, -) -> ILP { +) -> ILP { ILP::with_variables( bounds .iter() @@ -20,7 +20,7 @@ fn integer_ilp( .unwrap() } -fn solve_via_bool(source: &ILP) -> Option<(Vec, i64)> { +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 source_solution = reduction.extract_solution(&witness).unwrap(); @@ -59,7 +59,7 @@ fn test_ilp_i64_to_ilp_bool_maximize() { #[test] fn test_ilp_i64_to_ilp_bool_empty() { - let source = ILP::::empty(); + let source = ILP::::empty(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); assert_eq!(reduction.target_problem().num_vars(), 0); assert!(reduction.target_problem().constraints().is_empty()); diff --git a/src/unit_tests/rules/integerknapsack_ilp.rs b/src/unit_tests/rules/integerknapsack_ilp.rs index 8b421114a..e8fea50f7 100644 --- a/src/unit_tests/rules/integerknapsack_ilp.rs +++ b/src/unit_tests/rules/integerknapsack_ilp.rs @@ -1,6 +1,6 @@ #[cfg(feature = "example-db")] use super::canonical_rule_example_specs; -use crate::models::algebraic::{Comparison, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, Comparison, ObjectiveSense, ILP}; use crate::models::set::IntegerKnapsack; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::rules::{ReduceTo, ReductionResult}; @@ -9,7 +9,8 @@ use crate::solvers::ILPSolver; #[test] fn test_integerknapsack_to_ilp_closed_loop() { let source = IntegerKnapsack::new(vec![3, 4, 5], vec![4, 5, 7], 10).unwrap(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); assert_bf_vs_ilp(&source, &reduction); @@ -23,7 +24,8 @@ fn test_integerknapsack_to_ilp_closed_loop() { #[test] fn test_integerknapsack_to_ilp_structure() { let source = IntegerKnapsack::new(vec![3, 4, 5], vec![4, 5, 7], 10).unwrap(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 3); @@ -59,7 +61,8 @@ fn test_integerknapsack_to_ilp_structure() { #[test] fn test_integerknapsack_to_ilp_zero_capacity() { let source = IntegerKnapsack::new(vec![1, 2], vec![10, 20], 0).unwrap(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) diff --git a/src/unit_tests/rules/integralflowbundles_ilp.rs b/src/unit_tests/rules/integralflowbundles_ilp.rs index b9bea620c..0cb6c4c88 100644 --- a/src/unit_tests/rules/integralflowbundles_ilp.rs +++ b/src/unit_tests/rules/integralflowbundles_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{Comparison, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, Comparison, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -34,7 +34,7 @@ fn satisfying_config() -> Vec { fn test_integral_flow_bundles_to_ilp_structure() { let problem = yes_instance(); let reduction: ReductionIFBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 6); @@ -74,7 +74,7 @@ fn test_integral_flow_bundles_to_ilp_closed_loop() { assert!(problem.evaluate(&direct).unwrap()); let reduction: ReductionIFBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -87,7 +87,7 @@ fn test_integral_flow_bundles_to_ilp_closed_loop() { fn test_integral_flow_bundles_to_ilp_extract_solution_is_identity() { let problem = yes_instance(); let reduction: ReductionIFBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_eq!( reduction.extract_solution(&satisfying_config()).unwrap(), vec![1, 0, 1, 0, 0, 0] @@ -98,7 +98,7 @@ fn test_integral_flow_bundles_to_ilp_extract_solution_is_identity() { 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"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } @@ -106,7 +106,7 @@ fn test_integral_flow_bundles_to_ilp_unsat_instance_is_infeasible() { fn test_integral_flow_bundles_to_ilp_sink_requirement_constraint() { let problem = yes_instance(); let reduction: ReductionIFBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let sink_constraint = ilp @@ -122,6 +122,6 @@ fn test_integral_flow_bundles_to_ilp_sink_requirement_constraint() { fn test_integralflowbundles_to_ilp_bf_vs_ilp() { let problem = yes_instance(); let reduction: ReductionIFBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + 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/integralflowhomologousarcs_ilp.rs b/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs index 07888d3b8..51c5352ab 100644 --- a/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs +++ b/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -23,7 +24,8 @@ fn test_integralflowhomologousarcs_to_ilp_closed_loop() { .expect("source instance should be satisfiable"); assert!(source.evaluate(&direct).unwrap()); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -42,6 +44,7 @@ fn test_integralflowhomologousarcs_to_ilp_bf_vs_ilp() { 2, vec![(0, 1)], ); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + 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..c6cec2209 100644 --- a/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs +++ b/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -22,7 +23,8 @@ fn test_integralflowwithmultipliers_to_ilp_closed_loop() { .expect("source instance should be satisfiable"); assert!(source.evaluate(&direct).unwrap()); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -41,6 +43,7 @@ fn test_integralflowwithmultipliers_to_ilp_bf_vs_ilp() { vec![2, 2, 2, 2], 2, ); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + 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/ksatisfiability_directedtwocommodityintegralflow.rs b/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs index 6ad979fc3..ef995f0ab 100644 --- a/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs +++ b/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs @@ -1,6 +1,7 @@ #[cfg(feature = "example-db")] use super::canonical_rule_example_specs; use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::models::formula::CNFClause; #[cfg(feature = "example-db")] @@ -39,7 +40,8 @@ fn all_assignments(num_vars: usize) -> Vec> { fn solve_target_via_ilp( problem: &crate::models::graph::DirectedTwoCommodityIntegralFlow, ) -> Option> { - let reduction = ReduceTo::>::reduce_to(problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new().solve(reduction.target_problem()).ok()?; let extracted = reduction.extract_solution(&ilp_solution).unwrap(); problem.evaluate(&extracted).unwrap().0.then_some(extracted) diff --git a/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs b/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs index ef7624f3d..e242c81cf 100644 --- a/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs +++ b/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::models::formula::CNFClause; use crate::solvers::ILPSolver; @@ -140,7 +141,7 @@ fn test_ksatisfiability_to_feasible_register_assignment_closed_loop_via_ilp() { let source = issue_example(); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); - let fra_to_ilp = ReduceTo::>::reduce_to(reduction.target_problem()) + let fra_to_ilp = ReduceTo::>::reduce_to(reduction.target_problem()) .expect("reduction should succeed"); let ilp_solution = ILPSolver::new() @@ -167,7 +168,7 @@ fn test_ksatisfiability_to_feasible_register_assignment_unsatisfiable_instance() ); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); - let fra_to_ilp = ReduceTo::>::reduce_to(reduction.target_problem()) + let fra_to_ilp = ReduceTo::>::reduce_to(reduction.target_problem()) .expect("reduction should succeed"); assert!( diff --git a/src/unit_tests/rules/longestpath_ilp.rs b/src/unit_tests/rules/longestpath_ilp.rs index 8e23b37be..45d443943 100644 --- a/src/unit_tests/rules/longestpath_ilp.rs +++ b/src/unit_tests/rules/longestpath_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -36,7 +36,7 @@ fn simple_path_problem() -> LongestPath { fn test_reduction_creates_expected_ilp_shape() { let problem = simple_path_problem(); let reduction: ReductionLongestPathToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 7); @@ -67,7 +67,7 @@ fn test_longestpath_to_ilp_closed_loop_on_issue_example() { assert_eq!(best_value, Max(Some(20))); let reduction: ReductionLongestPathToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -82,7 +82,7 @@ fn test_longestpath_to_ilp_closed_loop_on_issue_example() { fn test_solution_extraction_from_handcrafted_ilp_assignment() { let problem = simple_path_problem(); let reduction: ReductionLongestPathToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // x_{0->1}, x_{1->0}, x_{1->2}, x_{2->1}, o_0, o_1, o_2 let target_solution = vec![1, 0, 1, 0, 0, 1, 2]; @@ -101,7 +101,7 @@ fn test_source_equals_target_uses_empty_path() { 1, ); let reduction: ReductionLongestPathToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -116,6 +116,6 @@ fn test_source_equals_target_uses_empty_path() { fn test_longestpath_to_ilp_bf_vs_ilp() { let problem = simple_path_problem(); let reduction: ReductionLongestPathToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + 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/maximumleafspanningtree_ilp.rs b/src/unit_tests/rules/maximumleafspanningtree_ilp.rs index be3068b63..1c5034599 100644 --- a/src/unit_tests/rules/maximumleafspanningtree_ilp.rs +++ b/src/unit_tests/rules/maximumleafspanningtree_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::graph::MaximumLeafSpanningTree; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -35,7 +35,7 @@ fn canonical_instance() -> MaximumLeafSpanningTree { fn test_reduction_creates_expected_ilp_shape() { let problem = small_instance(); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // n=4, m=4: num_vars = 3*4 + 4 = 16 @@ -49,7 +49,7 @@ fn test_reduction_creates_expected_ilp_shape() { fn test_maximumleafspanningtree_to_ilp_closed_loop() { let problem = small_instance(); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let bf = BruteForce::new(); @@ -69,7 +69,7 @@ fn test_maximumleafspanningtree_to_ilp_closed_loop() { fn test_maximumleafspanningtree_to_ilp_canonical_closed_loop() { let problem = canonical_instance(); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let bf = BruteForce::new(); @@ -87,7 +87,7 @@ fn test_maximumleafspanningtree_to_ilp_canonical_closed_loop() { fn test_solution_extraction_reads_edge_selector_prefix() { let problem = small_instance(); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // 16 variables total, first 4 are edge selectors let mut target_solution = vec![0; 16]; @@ -105,7 +105,7 @@ fn test_solution_extraction_reads_edge_selector_prefix() { fn test_reduce_and_solve_via_ilp() { let problem = canonical_instance(); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); @@ -118,7 +118,7 @@ fn test_reduce_and_solve_via_ilp() { fn test_maximumleafspanningtree_to_ilp_bf_vs_ilp() { let problem = canonical_instance(); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } @@ -127,7 +127,7 @@ 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 reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); @@ -140,7 +140,7 @@ 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 reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); @@ -161,7 +161,7 @@ fn test_maximumleafspanningtree_to_ilp_complete_graph() { let bf_value = problem.evaluate(&bf_solutions[0]).unwrap(); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); diff --git a/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs b/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs index e3725ece6..18af512bd 100644 --- a/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::graph::MinimumCapacitatedSpanningTree; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -45,7 +45,7 @@ fn canonical_instance() -> MinimumCapacitatedSpanningTree { fn test_reduction_creates_expected_ilp_shape() { let problem = small_instance(); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // m=5: num_vars = 5*5 = 25 @@ -57,7 +57,7 @@ fn test_reduction_creates_expected_ilp_shape() { fn test_minimumcapacitatedspanningtree_to_ilp_closed_loop() { let problem = small_instance(); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let bf = BruteForce::new(); @@ -76,7 +76,7 @@ fn test_minimumcapacitatedspanningtree_to_ilp_closed_loop() { fn test_minimumcapacitatedspanningtree_to_ilp_canonical_closed_loop() { let problem = canonical_instance(); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let bf = BruteForce::new(); @@ -94,7 +94,7 @@ fn test_minimumcapacitatedspanningtree_to_ilp_canonical_closed_loop() { fn test_solution_extraction_reads_edge_selector_prefix() { let problem = small_instance(); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // 25 variables total, first 5 are edge selectors let mut target_solution = vec![0; 25]; @@ -112,7 +112,7 @@ fn test_solution_extraction_reads_edge_selector_prefix() { fn test_minimumcapacitatedspanningtree_to_ilp_bf_vs_ilp() { let problem = canonical_instance(); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } @@ -128,7 +128,7 @@ fn test_minimumcapacitatedspanningtree_to_ilp_star_tree() { 1, // capacity = 1 forces star tree ); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); @@ -148,7 +148,7 @@ fn test_minimumcapacitatedspanningtree_to_ilp_path_graph() { 3, ); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); @@ -166,6 +166,6 @@ fn test_zero_requirement_vertex_still_must_be_connected() { 2, ); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } diff --git a/src/unit_tests/rules/minimumedgecostflow_ilp.rs b/src/unit_tests/rules/minimumedgecostflow_ilp.rs index 683d75059..7f8eed3cd 100644 --- a/src/unit_tests/rules/minimumedgecostflow_ilp.rs +++ b/src/unit_tests/rules/minimumedgecostflow_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -47,7 +47,7 @@ fn infeasible_instance() -> MinimumEdgeCostFlow { fn test_minimumedgecostflow_to_ilp_structure() { let problem = issue_instance(); let reduction: ReductionMECFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 6 arcs → 2*6 = 12 variables @@ -74,7 +74,7 @@ fn test_minimumedgecostflow_to_ilp_closed_loop() { assert_eq!(bf_value, Min(Some(3))); let reduction: ReductionMECFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -96,7 +96,7 @@ fn test_minimumedgecostflow_to_ilp_small_closed_loop() { assert_eq!(bf_value, Min(Some(8))); let reduction: ReductionMECFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -108,7 +108,7 @@ fn test_minimumedgecostflow_to_ilp_small_closed_loop() { fn test_minimumedgecostflow_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionMECFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "infeasible instance should produce infeasible ILP" @@ -119,7 +119,7 @@ fn test_minimumedgecostflow_to_ilp_infeasible() { fn test_minimumedgecostflow_to_ilp_bf_vs_ilp() { let problem = issue_instance(); let reduction: ReductionMECFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } @@ -127,7 +127,7 @@ fn test_minimumedgecostflow_to_ilp_bf_vs_ilp() { fn test_minimumedgecostflow_to_ilp_extract_solution() { let problem = issue_instance(); let reduction: ReductionMECFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Manually construct a target solution: route 1 via v2, 2 via v3 // f = [0, 1, 2, 0, 1, 2], y = [0, 1, 1, 0, 1, 1] diff --git a/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs b/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs index eb9e8aa51..59dd53eaf 100644 --- a/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs +++ b/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -10,7 +11,7 @@ fn feedback_arc_set_solves_through_integer_binary_ilp_and_qubo() { let source = MinimumFeedbackArcSet::new(DirectedGraph::new(2, vec![(0, 1), (1, 0)]), vec![2_i64, 5]); - let integer = ReduceTo::>::reduce_to(&source).unwrap(); + let integer = ReduceTo::>::reduce_to(&source).unwrap(); let binary = ReduceTo::>::reduce_to(integer.target_problem()).unwrap(); let qubo = ReduceTo::>::reduce_to(binary.target_problem()).unwrap(); let optimum = BruteForce::new() @@ -34,7 +35,7 @@ fn test_reduction_creates_valid_ilp() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); let reduction: ReductionFASToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // m + n = 3 + 3 = 6 variables (3 binary y_a + 3 integer o_v) @@ -55,7 +56,7 @@ fn test_minimumfeedbackarcset_to_ilp_bf_vs_ilp() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); let reduction: ReductionFASToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let bf = BruteForce::new(); @@ -81,7 +82,7 @@ fn test_solution_extraction() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); let reduction: ReductionFASToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Simulate ILP solution: y_0=0, y_1=0, y_2=1, o_0=0, o_1=1, o_2=2 let ilp_solution = vec![0, 0, 1, 0, 1, 2]; @@ -101,7 +102,7 @@ fn test_minimumfeedbackarcset_to_ilp_trivial() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 2]); let reduction: ReductionFASToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // m=2, n=3 → 5 variables; 2 + 3 + 2 = 7 constraints diff --git a/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs b/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs index 8b47862ae..e37eb44cb 100644 --- a/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs +++ b/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -10,7 +11,7 @@ fn test_reduction_creates_valid_ilp() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 2n = 6 variables (3 binary x_i + 3 integer o_i) @@ -27,7 +28,7 @@ fn test_minimumfeedbackvertexset_to_ilp_closed_loop() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let bf = BruteForce::new(); @@ -76,7 +77,7 @@ fn test_cycle_of_triangles() { let graph = DirectedGraph::new(9, arcs); let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 9]); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Verify ILP structure @@ -101,7 +102,7 @@ fn test_dag_no_removal() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -119,7 +120,7 @@ fn test_single_vertex() { let graph = DirectedGraph::new(1, vec![]); let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64]); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 2); @@ -141,7 +142,7 @@ fn test_weighted() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackVertexSet::new(graph, vec![10, 1, 10]); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Check that weights are correctly transferred to objective @@ -172,7 +173,7 @@ fn test_two_disjoint_cycles() { let bf_size = problem.evaluate(&bf_solutions[0]).unwrap(); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); @@ -189,7 +190,7 @@ fn test_solution_extraction() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Simulate ILP solution: x_0=1, x_1=0, x_2=0, o_0=0, o_1=0, o_2=1 let ilp_solution = vec![1, 0, 0, 0, 0, 1]; @@ -205,6 +206,6 @@ 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 reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + 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/minimumgraphbandwidth_ilp.rs b/src/unit_tests/rules/minimumgraphbandwidth_ilp.rs index 9eec5a8d3..af0d5374d 100644 --- a/src/unit_tests/rules/minimumgraphbandwidth_ilp.rs +++ b/src/unit_tests/rules/minimumgraphbandwidth_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -8,7 +9,7 @@ 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 reduction: ReductionMGBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // num_x=16, pos_v=4, B=1, total=21 assert_eq!(ilp.num_vars(), 21); @@ -30,7 +31,7 @@ fn test_minimumgraphbandwidth_to_ilp_closed_loop() { // Solve via ILP let reduction: ReductionMGBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -56,7 +57,7 @@ fn test_minimumgraphbandwidth_to_ilp_path() { let problem = MinimumGraphBandwidth::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); let reduction: ReductionMGBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -75,7 +76,7 @@ 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 reduction: ReductionMGBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } @@ -85,6 +86,6 @@ fn test_minimumgraphbandwidth_to_ilp_cycle() { let problem = MinimumGraphBandwidth::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)])); let reduction: ReductionMGBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + 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/minimumweightdecoding_ilp.rs b/src/unit_tests/rules/minimumweightdecoding_ilp.rs index 09eb28337..c4d322a78 100644 --- a/src/unit_tests/rules/minimumweightdecoding_ilp.rs +++ b/src/unit_tests/rules/minimumweightdecoding_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -35,7 +35,7 @@ fn infeasible_instance() -> MinimumWeightDecoding { fn test_minimumweightdecoding_to_ilp_structure() { let problem = issue_instance(); let reduction: ReductionMinimumWeightDecodingToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 4 cols + 3 rows = 7 variables @@ -61,7 +61,7 @@ fn test_minimumweightdecoding_to_ilp_closed_loop() { assert_eq!(bf_value, Min(Some(1))); let reduction: ReductionMinimumWeightDecodingToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -83,7 +83,7 @@ fn test_minimumweightdecoding_to_ilp_small_closed_loop() { assert_eq!(bf_value, Min(Some(1))); let reduction: ReductionMinimumWeightDecodingToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -95,7 +95,7 @@ fn test_minimumweightdecoding_to_ilp_small_closed_loop() { fn test_minimumweightdecoding_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionMinimumWeightDecodingToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "infeasible instance should produce infeasible ILP" @@ -106,7 +106,7 @@ fn test_minimumweightdecoding_to_ilp_infeasible() { fn test_minimumweightdecoding_to_ilp_bf_vs_ilp() { let problem = issue_instance(); let reduction: ReductionMinimumWeightDecodingToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } @@ -114,7 +114,7 @@ fn test_minimumweightdecoding_to_ilp_bf_vs_ilp() { fn test_minimumweightdecoding_to_ilp_extract_solution() { let problem = issue_instance(); let reduction: ReductionMinimumWeightDecodingToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Manually construct a valid target solution: x=[0,0,1,0], k=[0,0,0] // (k_i values are the integer slack from mod-2) diff --git a/src/unit_tests/rules/minmaxmulticenter_ilp.rs b/src/unit_tests/rules/minmaxmulticenter_ilp.rs index 743658bfb..517661794 100644 --- a/src/unit_tests/rules/minmaxmulticenter_ilp.rs +++ b/src/unit_tests/rules/minmaxmulticenter_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::graph::MinMaxMulticenter; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; @@ -16,7 +16,7 @@ fn test_reduction_creates_valid_ilp() { 1, ); let reduction: ReductionMMCToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // num_vars = n + n^2 + 1 = 3 + 9 + 1 = 13 assert_eq!(ilp.num_vars(), 13, "n + n^2 + 1 variables"); @@ -43,7 +43,7 @@ fn test_minmaxmulticenter_to_ilp_bf_vs_ilp() { 1, ); let reduction: ReductionMMCToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let bf = BruteForce::new(); @@ -72,7 +72,7 @@ fn test_solution_extraction() { 1, ); let reduction: ReductionMMCToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Manually construct a valid ILP solution: // x = [0, 1, 0]; each vertex assigned to center 1; z = 1 @@ -104,7 +104,7 @@ fn test_minmaxmulticenter_to_ilp_weighted() { assert_eq!(bf_value, Min(Some(100))); let reduction: ReductionMMCToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); @@ -117,7 +117,7 @@ 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 reduction: ReductionMMCToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // num_vars = 1 + 1 + 1 = 3 assert_eq!(ilp.num_vars(), 3); diff --git a/src/unit_tests/rules/mixedchinesepostman_ilp.rs b/src/unit_tests/rules/mixedchinesepostman_ilp.rs index 46e5817de..24e44a7bb 100644 --- a/src/unit_tests/rules/mixedchinesepostman_ilp.rs +++ b/src/unit_tests/rules/mixedchinesepostman_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -19,7 +20,8 @@ fn test_mixedchinesepostman_to_ilp_closed_loop() { .expect("source instance should have an optimal solution"); assert!(source.evaluate(&direct).unwrap().0.is_some()); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -41,7 +43,8 @@ fn test_mixedchinesepostman_to_ilp_bf_vs_ilp() { let bf_value = source.evaluate(&bf_value_solution).unwrap(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -67,7 +70,8 @@ fn test_mixedchinesepostman_to_ilp_weighted() { let bf_value = source.evaluate(&bf_value_solution).unwrap(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -91,7 +95,7 @@ fn test_mixedchinesepostman_to_ilp_with_isolated_vertices() { vec![4, 5, 1, 12, 9], vec![6, 1, 13, 7], ); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let ilp_solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); diff --git a/src/unit_tests/rules/multiplechoicebranching_ilp.rs b/src/unit_tests/rules/multiplechoicebranching_ilp.rs index 6166a1180..10460819f 100644 --- a/src/unit_tests/rules/multiplechoicebranching_ilp.rs +++ b/src/unit_tests/rules/multiplechoicebranching_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -14,7 +15,7 @@ fn test_multiplechoicebranching_to_ilp_closed_loop() { threshold, ); let expected = BruteForce::new().solve(&problem).unwrap(); - let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); match expected { Some(_) => { let target = ILPSolver::new().solve(reduction.target_problem()).unwrap(); @@ -34,7 +35,7 @@ fn test_multiplechoicebranching_to_ilp_rejects_forced_cycle() { vec![vec![0], vec![1]], 2, ); - let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } @@ -46,7 +47,7 @@ fn test_multiplechoicebranching_to_ilp_size() { vec![vec![0, 1], vec![2, 3]], 3, ); - let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); assert_eq!(reduction.target_problem().num_vars(), 7); assert_eq!(reduction.target_problem().num_constraints(), 17); } @@ -54,7 +55,7 @@ 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 reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let target = ILPSolver::new().solve(reduction.target_problem()).unwrap(); assert_eq!( reduction.extract_solution(&target).unwrap(), diff --git a/src/unit_tests/rules/openshopscheduling_ilp.rs b/src/unit_tests/rules/openshopscheduling_ilp.rs index 27b813623..bee202188 100644 --- a/src/unit_tests/rules/openshopscheduling_ilp.rs +++ b/src/unit_tests/rules/openshopscheduling_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::models::misc::OpenShopScheduling; use crate::solvers::ILPSolver; @@ -14,12 +15,12 @@ fn small_instance() -> OpenShopScheduling { #[test] fn test_decision_openshopscheduling_to_ilp_bound_is_a_constraint() { let inner = small_instance(); - let optimization = ReduceTo::>::reduce_to(&inner).unwrap(); + let optimization = ReduceTo::>::reduce_to(&inner).unwrap(); let solver = ILPSolver::new(); let optimal = solver.solve(optimization.target_problem()).unwrap(); for bound in [-1, 2, 3, 4] { let source = Decision::new(inner.clone(), bound); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); assert!(target.objective().is_empty()); assert_eq!(target.num_vars(), optimization.target_problem().num_vars()); @@ -61,7 +62,7 @@ fn medium_instance() -> OpenShopScheduling { fn test_openshopscheduling_to_ilp_structure_small() { let p = small_instance(); let reduction: ReductionOSSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp = reduction.target_problem(); // n=2, m=2: @@ -97,7 +98,7 @@ fn test_openshopscheduling_to_ilp_structure_small() { fn test_openshopscheduling_to_ilp_closed_loop_small() { let p = small_instance(); let reduction: ReductionOSSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -116,7 +117,7 @@ fn test_openshopscheduling_to_ilp_closed_loop_small() { fn test_openshopscheduling_to_ilp_closed_loop_medium() { let p = medium_instance(); let reduction: ReductionOSSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -139,7 +140,7 @@ fn test_openshopscheduling_to_ilp_closed_loop_medium() { fn test_openshopscheduling_to_ilp_extract_solution_respects_start_times() { let p = small_instance(); let reduction: ReductionOSSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let target_solution = vec![1, 0, 0, 1, 1, 0, 1, 0, 3]; let extracted = reduction.extract_solution(&target_solution).unwrap(); assert_eq!(extracted, vec![0, 1, 1, 0]); @@ -153,7 +154,7 @@ fn test_openshopscheduling_to_ilp_single_job() { // 1 job, 2 machines: trivial, makespan = sum of processing times let p = OpenShopScheduling::new(2, vec![vec![3, 4]]); let reduction: ReductionOSSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -168,7 +169,7 @@ fn test_openshopscheduling_to_ilp_single_machine() { // 3 jobs, 1 machine: serial schedule, makespan = sum of all processing times let p = OpenShopScheduling::new(1, vec![vec![2], vec![3], vec![1]]); let reduction: ReductionOSSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); diff --git a/src/unit_tests/rules/optimallineararrangement_ilp.rs b/src/unit_tests/rules/optimallineararrangement_ilp.rs index c45f0bf41..ca28a7d1c 100644 --- a/src/unit_tests/rules/optimallineararrangement_ilp.rs +++ b/src/unit_tests/rules/optimallineararrangement_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -8,7 +9,7 @@ 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 reduction: ReductionOLAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // num_x=16, p_v=4, z_e=3, total=23 assert_eq!(ilp.num_vars(), 23); @@ -29,7 +30,7 @@ fn test_optimallineararrangement_to_ilp_closed_loop() { // Solve via ILP let reduction: ReductionOLAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -59,7 +60,7 @@ fn test_optimallineararrangement_to_ilp_with_chords() { // Solve via ILP let reduction: ReductionOLAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -72,7 +73,7 @@ fn test_optimallineararrangement_to_ilp_with_chords() { fn test_solution_extraction() { let problem = OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); let reduction: ReductionOLAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -85,6 +86,6 @@ fn test_solution_extraction() { fn test_optimallineararrangement_to_ilp_bf_vs_ilp() { let problem = OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); let reduction: ReductionOLAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + 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/partition_openshopscheduling.rs b/src/unit_tests/rules/partition_openshopscheduling.rs index afab0667b..0ed35ce0f 100644 --- a/src/unit_tests/rules/partition_openshopscheduling.rs +++ b/src/unit_tests/rules/partition_openshopscheduling.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::models::decision::Decision; use crate::models::misc::{OpenShopScheduling, Partition}; @@ -6,7 +7,8 @@ use crate::solvers::ILPSolver; use crate::traits::Problem; fn solve_target(target: &OpenShopScheduling) -> Vec { - let reduction = ReduceTo::>::reduce_to(target).expect("ILP reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(target) + .expect("ILP reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("open-shop target should be feasible"); diff --git a/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs b/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs index ac4111f81..7076d515b 100644 --- a/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs +++ b/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -22,7 +23,8 @@ fn test_pathconstrainednetworkflow_to_ilp_closed_loop() { .expect("source instance should be satisfiable"); assert!(source.evaluate(&direct).unwrap()); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -41,6 +43,7 @@ fn test_pathconstrainednetworkflow_to_ilp_bf_vs_ilp() { vec![vec![0, 1], vec![2]], 2, ); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + 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/preemptivescheduling_ilp.rs b/src/unit_tests/rules/preemptivescheduling_ilp.rs index cd0a78b23..4f14b120b 100644 --- a/src/unit_tests/rules/preemptivescheduling_ilp.rs +++ b/src/unit_tests/rules/preemptivescheduling_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::solvers::ILPSolver; use crate::traits::Problem; @@ -25,7 +26,7 @@ fn test_preemptivescheduling_to_ilp_structure() { let p = small_instance(); // n=2, D_max=2 → 2*2+1 = 5 variables let reduction: ReductionPSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 5, "expected n*D_max+1 = 5 variables"); assert_eq!( @@ -45,7 +46,7 @@ fn test_preemptivescheduling_to_ilp_structure() { fn test_preemptivescheduling_to_ilp_closed_loop() { let p = small_instance(); let reduction: ReductionPSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -62,7 +63,7 @@ fn test_solve_via_registered_integer_ilp_pipeline() { let problem = small_instance(); let solution = ILPSolver::new() .solve(&problem) - .expect("direct ILP reduction should be solvable"); + .expect("direct ILP reduction should be solvable"); assert!(problem.evaluate(&solution).unwrap().0.is_some()); } @@ -71,7 +72,7 @@ fn test_solve_via_registered_integer_ilp_pipeline() { fn test_preemptivescheduling_to_ilp_medium_closed_loop() { let p = medium_instance(); let reduction: ReductionPSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -98,7 +99,7 @@ fn test_preemptivescheduling_to_ilp_infeasible() { // Use a cycle-free precedence that is always schedulable. let p = PreemptiveScheduling::new(vec![1, 1], 1, vec![(0, 1)]).unwrap(); let reduction: ReductionPSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let sol = ILPSolver::new().solve(reduction.target_problem()); // 1 processor, t0 at slot 0, t1 at slot 1 → always feasible assert!(sol.is_ok(), "should be feasible"); @@ -112,7 +113,7 @@ fn test_preemptivescheduling_to_ilp_extract_solution() { // x_{0,0}=1, x_{0,1}=0, x_{1,0}=0, x_{1,1}=1, M=2 let p = small_instance(); let reduction: ReductionPSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp_solution = vec![1, 0, 0, 1, 2]; // last element is M let extracted = reduction.extract_solution(&ilp_solution).unwrap(); assert_eq!(extracted, vec![vec![true, false], vec![false, true]]); diff --git a/src/unit_tests/rules/reduction_path_parity.rs b/src/unit_tests/rules/reduction_path_parity.rs index 5dc4cd245..cb1490a2a 100644 --- a/src/unit_tests/rules/reduction_path_parity.rs +++ b/src/unit_tests/rules/reduction_path_parity.rs @@ -2,6 +2,7 @@ //! Verifies that explicit chained reductions via `reduce_along_path` //! produce correct solutions matching direct source solves. +use crate::models::algebraic::Bounded; use crate::models::algebraic::QUBO; use crate::models::graph::{MaxCut, SpinGlass}; use crate::models::misc::Factoring; @@ -141,7 +142,8 @@ fn test_jl_parity_factoring_to_spinglass_path() { use crate::models::algebraic::ILP; use crate::rules::traits::{ReduceTo, ReductionResult}; let ilp_solver = ILPSolver::new(); - let reduction = ReduceTo::>::reduce_to(&factoring).expect("reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(&factoring) + .expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ilp_solver .solve(ilp) diff --git a/src/unit_tests/rules/registersufficiency_ilp.rs b/src/unit_tests/rules/registersufficiency_ilp.rs index beaef71d7..c2b08e675 100644 --- a/src/unit_tests/rules/registersufficiency_ilp.rs +++ b/src/unit_tests/rules/registersufficiency_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::misc::RegisterSufficiency; use crate::solvers::ILPSolver; use crate::traits::Problem; @@ -33,7 +34,8 @@ fn canonical_example() -> RegisterSufficiency { #[test] fn test_register_sufficiency_to_ilp_structure() { let source = feasible_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 62); @@ -45,7 +47,8 @@ fn test_register_sufficiency_to_ilp_structure() { #[test] fn test_register_sufficiency_to_ilp_closed_loop() { let source = feasible_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) @@ -61,7 +64,8 @@ fn test_register_sufficiency_to_ilp_closed_loop() { #[test] fn test_register_sufficiency_to_ilp_infeasible() { let source = infeasible_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), @@ -72,7 +76,8 @@ fn test_register_sufficiency_to_ilp_infeasible() { #[test] fn test_register_sufficiency_to_ilp_bf_vs_ilp() { let source = feasible_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } @@ -107,7 +112,8 @@ fn test_register_sufficiency_to_ilp_canonical_example_spec() { assert_eq!(example.solutions.len(), 1); let source = canonical_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let solution = &example.solutions[0]; let source_config: Vec = serde_json::from_value(solution.source_config.clone()).unwrap(); let target_config: Vec = serde_json::from_value(solution.target_config.clone()).unwrap(); diff --git a/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs b/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs index 737aec834..b754b7ed2 100644 --- a/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs +++ b/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -8,7 +8,7 @@ use crate::types::Or; fn test_reduction_creates_valid_ilp() { let problem = RootedTreeStorageAssignment::new(3, vec![vec![0, 1], vec![1, 2]], 1); let reduction: ReductionRTSAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // n=3, r=2 (both subsets have size 2) @@ -31,7 +31,7 @@ fn test_rootedtreestorageassignment_to_ilp_bf_vs_ilp() { .unwrap_or(Or(false)); let reduction: ReductionRTSAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_result = ilp_solver.solve(reduction.target_problem()); @@ -62,7 +62,7 @@ fn test_rootedtreestorageassignment_to_ilp_infeasible() { let bf_witness = bf.solve(&problem).unwrap(); let reduction: ReductionRTSAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_result = ilp_solver.solve(reduction.target_problem()); assert!(bf_witness.is_none(), "source should be infeasible"); @@ -73,7 +73,7 @@ fn test_rootedtreestorageassignment_to_ilp_infeasible() { fn test_solution_extraction() { let problem = RootedTreeStorageAssignment::new(3, vec![vec![0, 1, 2]], 0); let reduction: ReductionRTSAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) diff --git a/src/unit_tests/rules/ruralpostman_ilp.rs b/src/unit_tests/rules/ruralpostman_ilp.rs index ac981b878..6df6638c3 100644 --- a/src/unit_tests/rules/ruralpostman_ilp.rs +++ b/src/unit_tests/rules/ruralpostman_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -19,7 +20,8 @@ fn test_ruralpostman_to_ilp_closed_loop() { .expect("source instance should have an optimal solution"); assert!(source.evaluate(&direct).unwrap().0.is_some()); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -45,7 +47,8 @@ fn test_ruralpostman_to_ilp_optimization() { let bf_value = source.evaluate(&bf_witness).unwrap(); // ILP reduction optimal - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -66,7 +69,8 @@ fn test_ruralpostman_to_ilp_bf_vs_ilp() { vec![1, 1, 1], vec![0], ); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } @@ -77,7 +81,7 @@ fn test_ruralpostman_empty_required_set_extracts_zero_multiplicities() { vec![4, 7], vec![], ); - let reduction = ReduceTo::>::reduce_to(&source).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/schedulingtominimizeweightedcompletiontime_ilp.rs b/src/unit_tests/rules/schedulingtominimizeweightedcompletiontime_ilp.rs index eaab98e84..52f8d9957 100644 --- a/src/unit_tests/rules/schedulingtominimizeweightedcompletiontime_ilp.rs +++ b/src/unit_tests/rules/schedulingtominimizeweightedcompletiontime_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::misc::SchedulingToMinimizeWeightedCompletionTime; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -10,7 +10,7 @@ fn test_reduction_creates_valid_ilp_structure() { // 3 tasks, 2 processors let problem = SchedulingToMinimizeWeightedCompletionTime::new(vec![1, 2, 3], vec![4, 2, 1], 2); let reduction: ReductionSMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // n=3, m=2: x vars = 3*2=6, C vars = 3, y vars = 3*2/2=3, total=12 @@ -37,7 +37,7 @@ fn test_reduction_creates_valid_ilp_structure() { fn test_solution_extraction() { let problem = SchedulingToMinimizeWeightedCompletionTime::new(vec![1, 2], vec![3, 1], 2); let reduction: ReductionSMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Build a manual ILP solution: // x_{0,0}=1, x_{0,1}=0, x_{1,0}=0, x_{1,1}=1 => task 0 on P0, task 1 on P1 @@ -74,7 +74,7 @@ fn test_ilp_matches_bruteforce_small() { let bf_value = problem.evaluate(&bf_witness).unwrap(); let reduction: ReductionSMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); @@ -93,7 +93,7 @@ fn test_issue_example_closed_loop() { ); let reduction: ReductionSMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); @@ -105,7 +105,7 @@ fn test_issue_example_closed_loop() { fn test_single_task_single_processor() { let problem = SchedulingToMinimizeWeightedCompletionTime::new(vec![5], vec![3], 1); let reduction: ReductionSMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); @@ -127,7 +127,7 @@ fn test_equal_tasks_multiple_processors() { let bf_value = problem.evaluate(&bf_witness).unwrap(); let reduction: ReductionSMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); diff --git a/src/unit_tests/rules/sequencingtominimizemaximumcumulativecost_ilp.rs b/src/unit_tests/rules/sequencingtominimizemaximumcumulativecost_ilp.rs index 617788d35..b3ce0d01f 100644 --- a/src/unit_tests/rules/sequencingtominimizemaximumcumulativecost_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizemaximumcumulativecost_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -7,7 +8,8 @@ use crate::traits::Problem; #[test] fn test_sequencingtominimizemaximumcumulativecost_to_ilp_closed_loop() { let problem = SequencingToMinimizeMaximumCumulativeCost::new(vec![2, -1, 3, -2], vec![(0, 2)]); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Brute-force the source to get the optimal value let bf = BruteForce::new(); @@ -34,7 +36,8 @@ fn test_sequencingtominimizemaximumcumulativecost_to_ilp_closed_loop() { #[test] fn test_sequencingtominimizemaximumcumulativecost_to_ilp_bf_vs_ilp() { let problem = SequencingToMinimizeMaximumCumulativeCost::new(vec![2, -1, 3, -2], vec![(0, 2)]); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf_witness = BruteForce::new() .solve(&problem) @@ -52,7 +55,8 @@ fn test_sequencingtominimizemaximumcumulativecost_to_ilp_bf_vs_ilp() { #[test] fn test_sequencingtominimizemaximumcumulativecost_to_ilp_no_precedences() { let problem = SequencingToMinimizeMaximumCumulativeCost::new(vec![3, -2, 1], vec![]); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); diff --git a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index 62b7503de..9a4340b8a 100644 --- a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -9,7 +9,7 @@ use crate::types::Min; fn test_reduction_creates_expected_ilp_shape() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![2, 1], vec![3, 5], vec![]); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 2 completion variables + 1 pair-order variable. @@ -28,7 +28,7 @@ fn test_variable_layout_helpers() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![2, 1, 3], vec![3, 5, 1], vec![(0, 2)]); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_eq!(reduction.completion_var(0), 0); assert_eq!(reduction.completion_var(2), 2); @@ -41,7 +41,7 @@ fn test_variable_layout_helpers() { fn test_extract_solution_encodes_schedule_as_lehmer_code() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![2, 1], vec![3, 5], vec![]); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Completion times C0 = 3, C1 = 1 imply schedule [1, 0]. // y_{0,1} = 0 means task 1 before task 0. @@ -58,7 +58,7 @@ fn test_issue_example_closed_loop() { vec![(0, 2), (1, 4)], ); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); @@ -84,7 +84,7 @@ fn test_ilp_matches_bruteforce_optimum() { let brute_force_metric = problem.evaluate(&brute_force_solution).unwrap(); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); @@ -101,7 +101,7 @@ fn test_cyclic_precedence_instance_is_infeasible() { vec![(0, 1), (1, 0)], ); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert!( @@ -115,7 +115,7 @@ fn test_reduction_rejects_total_processing_time_outside_i64_domain() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![i64::MAX, 1], vec![1, 1], vec![]); assert!(matches!( - ReduceTo::>::reduce_to(&problem), + ReduceTo::>::reduce_to(&problem), Err(crate::rules::ReductionError::IntegerOverflow { .. }) )); } @@ -124,7 +124,7 @@ fn test_reduction_rejects_total_processing_time_outside_i64_domain() { fn test_reduction_preserves_a_weight_outside_exact_f64_integer_range() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![1], vec![(1i64 << 53) + 1], vec![]); - let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); assert_eq!( reduction.target_problem().objective(), &[(0, (1i64 << 53) + 1)] @@ -135,7 +135,7 @@ fn test_reduction_preserves_a_weight_outside_exact_f64_integer_range() { fn test_reduction_preserves_large_weighted_completion_objective() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![1, 1], vec![1 << 52, 1 << 52], vec![]); - let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); assert_eq!( reduction.target_problem().objective(), &[(0, 1 << 52), (1, 1 << 52)] @@ -150,7 +150,7 @@ fn test_ilp_pipeline_matches_source_optimum() { vec![(0, 2), (1, 4)], ); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); @@ -164,6 +164,6 @@ fn test_ilp_pipeline_matches_source_optimum() { fn test_sequencingtominimizeweightedcompletiontime_to_ilp_bf_vs_ilp() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![2, 1], vec![3, 5], vec![]); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + 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/sequencingtominimizeweightedtardiness_ilp.rs b/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs index 3768789a1..072d5b2d1 100644 --- a/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -8,9 +9,10 @@ use crate::types::Or; fn test_sequencingtominimizeweightedtardiness_to_ilp_closed_loop() { let problem = SequencingToMinimizeWeightedTardiness::new(vec![3, 4, 2], vec![2, 3, 1], vec![5, 8, 4], 10); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - // Use ILPSolver directly (BruteForce cannot enumerate `ILP`) + // Use ILPSolver directly (BruteForce cannot enumerate `ILP`) let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); @@ -22,7 +24,8 @@ fn test_sequencingtominimizeweightedtardiness_to_ilp_closed_loop() { 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); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf_witness = BruteForce::new() .solve(&problem) @@ -42,7 +45,8 @@ 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); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "infeasible STMWT should produce infeasible ILP" @@ -58,7 +62,8 @@ fn test_sequencingtominimizeweightedtardiness_to_ilp_no_tardiness() { vec![10, 10, 10], 0, ); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); diff --git a/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs b/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs index 8fe434aab..0de30cfae 100644 --- a/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -21,7 +21,7 @@ fn simple_path_problem() -> ShortestWeightConstrainedPath { fn test_reduction_creates_valid_ilp() { let problem = simple_path_problem(); let reduction: ReductionSWCPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 2 edges => 4 arc vars + 3 order vars = 7 @@ -50,7 +50,7 @@ fn test_shortestweightconstrainedpath_to_ilp_bf_vs_ilp() { let bf_value = problem.evaluate(&bf_value_solution).unwrap(); let reduction: ReductionSWCPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_result = ilp_solver.solve(reduction.target_problem()); @@ -72,7 +72,7 @@ fn test_shortestweightconstrainedpath_to_ilp_bf_vs_ilp() { fn test_solution_extraction() { let problem = simple_path_problem(); let reduction: ReductionSWCPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Handcrafted ILP solution: path 0->1->2 // a_{0,fwd}=1, a_{0,rev}=0, a_{1,fwd}=1, a_{1,rev}=0, o_0=0, o_1=1, o_2=2 @@ -96,7 +96,7 @@ fn test_shortestweightconstrainedpath_to_ilp_trivial() { 4, // weight_bound ); let reduction: ReductionSWCPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) diff --git a/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs b/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs index 0fd893b32..a9062a5f4 100644 --- a/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs @@ -19,7 +19,7 @@ fn small_instance() -> StrongConnectivityAugmentation { fn test_strongconnectivityaugmentation_to_ilp_closed_loop() { let source = small_instance(); let reduction: ReductionSCAToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Solve source with brute force @@ -42,7 +42,7 @@ fn test_strongconnectivityaugmentation_to_ilp_closed_loop() { fn test_extract_solution() { let source = small_instance(); let reduction: ReductionSCAToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("ILP should be solvable"); @@ -55,7 +55,7 @@ fn test_extract_solution() { fn test_trivial_single_vertex() { let source = StrongConnectivityAugmentation::new(DirectedGraph::new(1, vec![]), vec![], 0); let reduction: ReductionSCAToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("trivial should be solvable"); @@ -68,7 +68,7 @@ fn test_single_vertex_candidate_selection_must_still_respect_budget() { let source = StrongConnectivityAugmentation::new(DirectedGraph::new(1, vec![]), vec![(0, 0, 1)], 0); let reduction: ReductionSCAToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let mut config = vec![0; ilp.num_vars()]; config[0] = 1; @@ -92,7 +92,7 @@ fn test_infeasible_budget() { 5, ); let reduction: ReductionSCAToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); assert!(solver.solve(ilp).is_err()); @@ -102,6 +102,6 @@ fn test_infeasible_budget() { fn test_strongconnectivityaugmentation_to_ilp_bf_vs_ilp() { let source = small_instance(); let reduction: ReductionSCAToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + 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/undirectedflowlowerbounds_ilp.rs b/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs index 9180733e6..bc4f5d3a0 100644 --- a/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -28,7 +28,7 @@ fn sink_self_loop_cannot_supply_net_flow() { 1, ); assert!(BruteForce::new().solve(&source).unwrap().is_none()); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let assignment = vec![1, 0, 1]; assert!(reduction .target_problem() @@ -60,7 +60,7 @@ fn infeasible_instance() -> UndirectedFlowLowerBounds { fn test_undirectedflowlowerbounds_to_ilp_structure() { let problem = feasible_instance(); let reduction: ReductionUFLBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 2 edges → 3*2 = 6 variables @@ -83,7 +83,7 @@ fn test_undirectedflowlowerbounds_to_ilp_closed_loop() { ); let reduction: ReductionUFLBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -101,7 +101,7 @@ fn test_undirectedflowlowerbounds_to_ilp_closed_loop() { fn test_undirectedflowlowerbounds_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionUFLBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "infeasible instance should produce infeasible ILP" @@ -112,7 +112,7 @@ fn test_undirectedflowlowerbounds_to_ilp_infeasible() { fn test_undirectedflowlowerbounds_to_ilp_extract_solution() { let problem = feasible_instance(); let reduction: ReductionUFLBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // f_{01}=1, f_{10}=0, f_{12}=1, f_{21}=0, z_0=1, z_1=1 // z_e=1 means u→v direction; model expects config[e]=0 for u→v → extract returns 1-z_e @@ -130,6 +130,6 @@ fn test_undirectedflowlowerbounds_to_ilp_extract_solution() { fn test_undirectedflowlowerbounds_to_ilp_bf_vs_ilp() { let problem = feasible_instance(); let reduction: ReductionUFLBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + 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/undirectedtwocommodityintegralflow_ilp.rs b/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs index bf0efae03..1416f8fd9 100644 --- a/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -33,7 +33,7 @@ fn sink_self_loop_cannot_supply_either_commodity() { second, ); assert!(BruteForce::new().solve(&source).unwrap().is_none()); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let assignment = vec![first, 0, second, 0, 1, 1]; assert!(reduction .target_problem() @@ -69,7 +69,7 @@ fn infeasible_instance() -> UndirectedTwoCommodityIntegralFlow { fn test_undirectedtwocommodityintegralflow_to_ilp_structure() { let problem = feasible_instance(); let reduction: ReductionU2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 3 edges → 4 flow vars + 2 direction vars per edge = 18 variables. @@ -83,7 +83,7 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_structure() { fn test_undirectedtwocommodityintegralflow_to_ilp_overhead_matches_target() { let problem = feasible_instance(); let reduction: ReductionU2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let entry = crate::rules::registry::reduction_entries() @@ -96,7 +96,7 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_overhead_matches_target() { .iter() .any(|(key, value)| *key == "variable" && *value == "i64") }) - .expect("U2CIF -> ILP reduction should be registered"); + .expect("U2CIF -> ILP reduction should be registered"); let source_size = problem.parameters(); let predicted = entry @@ -130,7 +130,7 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_closed_loop() { ); let reduction: ReductionU2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -146,7 +146,7 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_closed_loop() { fn test_undirectedtwocommodityintegralflow_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionU2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "infeasible flow instance should yield infeasible ILP" @@ -166,7 +166,7 @@ fn test_other_commodity_source_cannot_create_flow() { 0, ); let reduction: ReductionU2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } @@ -174,7 +174,7 @@ fn test_other_commodity_source_cannot_create_flow() { fn test_undirectedtwocommodityintegralflow_to_ilp_extract_solution() { let problem = feasible_instance(); let reduction: ReductionU2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Manual solution: edge 0 (0,2): f1_uv=1, f1_vu=0, f2_uv=0, f2_vu=0 // edge 1 (1,2): f1_uv=0, f1_vu=0, f2_uv=1, f2_vu=0 @@ -201,6 +201,6 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_extract_solution() { fn test_undirectedtwocommodityintegralflow_to_ilp_bf_vs_ilp() { let problem = feasible_instance(); let reduction: ReductionU2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } diff --git a/src/unit_tests/solvers/registry.rs b/src/unit_tests/solvers/registry.rs index c4480fa49..230871390 100644 --- a/src/unit_tests/solvers/registry.rs +++ b/src/unit_tests/solvers/registry.rs @@ -1,8 +1,16 @@ use super::*; use std::collections::BTreeMap; -const FLOAT_BOOL_VARIANT: &[(&str, &str)] = &[("variable", "bool"), ("coefficient", "f64")]; -const FLOAT_I64_VARIANT: &[(&str, &str)] = &[("variable", "i64"), ("coefficient", "f64")]; +const FLOAT_BOOL_VARIANT: &[(&str, &str)] = &[ + ("variable", "bool"), + ("coefficient", "f64"), + ("bounds", "general"), +]; +const FLOAT_I64_VARIANT: &[(&str, &str)] = &[ + ("variable", "i64"), + ("coefficient", "f64"), + ("bounds", "general"), +]; const NO_VARIANT: &[(&str, &str)] = &[]; #[test] @@ -264,6 +272,7 @@ fn solver_capability_registry_duplicate_ilp_registration_is_rejected_independent BTreeMap::from([ ("variable".to_string(), "bool".to_string()), ("coefficient".to_string(), "f64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), )]); for pipelines in [ @@ -316,6 +325,7 @@ fn solver_capability_registry_unknown_pipeline_variant_is_rejected() { BTreeMap::from([ ("variable".to_string(), "bool".to_string()), ("coefficient".to_string(), "i64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), )]); let error = build_registry( @@ -365,6 +375,7 @@ fn solver_capability_registry_pipeline_with_missing_exact_edge_is_rejected() { BTreeMap::from([ ("variable".to_string(), "bool".to_string()), ("coefficient".to_string(), "f64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), ), ]); @@ -390,6 +401,7 @@ fn solver_capability_registry_pipeline_must_stop_at_first_supported_ilp_node() { BTreeMap::from([ ("variable".to_string(), "bool".to_string()), ("coefficient".to_string(), "f64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), ), ExactProblemKey::new( @@ -397,6 +409,7 @@ fn solver_capability_registry_pipeline_must_stop_at_first_supported_ilp_node() { BTreeMap::from([ ("variable".to_string(), "i64".to_string()), ("coefficient".to_string(), "f64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), ), ]); @@ -455,7 +468,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"] + ["MaximumClique", "ILP"] ); let multihop_ilp = solver_capabilities(&key( @@ -478,9 +491,19 @@ fn solver_capability_registry_exposes_representative_capability_classes() { assert!(brute_force_only.customized.is_none()); assert!(brute_force_only.ilp.is_none()); - let ilp_itself = - solver_capabilities(&key("ILP", &[("variable", "bool"), ("coefficient", "i64")])).unwrap(); - assert_eq!(ilp_itself.ilp.unwrap().path_labels(), ["ILP"]); + let ilp_itself = solver_capabilities(&key( + "ILP", + &[ + ("variable", "bool"), + ("coefficient", "i64"), + ("bounds", "general"), + ], + )) + .unwrap(); + assert_eq!( + ilp_itself.ilp.unwrap().path_labels(), + ["ILP"] + ); } #[test] diff --git a/src/unit_tests/solvers/resolver.rs b/src/unit_tests/solvers/resolver.rs index f98171f99..9d40551e9 100644 --- a/src/unit_tests/solvers/resolver.rs +++ b/src/unit_tests/solvers/resolver.rs @@ -435,6 +435,7 @@ fn deterministic_solver_dispatch_integer_ilp_uses_native_terminal() { &BTreeMap::from([ ("variable".to_string(), "bool".to_string()), ("coefficient".to_string(), "i64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), serde_json::to_value(problem).unwrap(), ) @@ -444,7 +445,7 @@ fn deterministic_solver_dispatch_integer_ilp_uses_native_terminal() { assert_eq!( result.solver, SolverExecution::Ilp { - reduction_path: vec!["ILP".to_string()] + reduction_path: vec!["ILP".to_string()] } ); assert!(matches!( @@ -470,6 +471,7 @@ fn deterministic_solver_dispatch_ilp_infeasibility_does_not_fall_back() { &BTreeMap::from([ ("variable".to_string(), "bool".to_string()), ("coefficient".to_string(), "i64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), serde_json::to_value(problem).unwrap(), ) @@ -494,12 +496,12 @@ fn deterministic_solver_execution_has_stable_tagged_json_contract() { ); assert_eq!( serde_json::to_value(SolverExecution::Ilp { - reduction_path: vec!["Source".to_string(), "ILP".to_string()] + reduction_path: vec!["Source".to_string(), "ILP".to_string()] }) .unwrap(), serde_json::json!({ "kind": "ilp", - "reduction_path": ["Source", "ILP"] + "reduction_path": ["Source", "ILP"] }) ); assert_eq!( @@ -560,7 +562,7 @@ fn deterministic_solver_dispatch_fixed_multihop_pipeline_is_repeatable() { "MaximumIndependentSet", "MaximumIndependentSet", "MaximumSetPacking", - "ILP", + "ILP", ] ); } diff --git a/src/unit_tests/symbolic_parameter_contracts.rs b/src/unit_tests/symbolic_parameter_contracts.rs index c0411e1e0..49826259c 100644 --- a/src/unit_tests/symbolic_parameter_contracts.rs +++ b/src/unit_tests/symbolic_parameter_contracts.rs @@ -1,4 +1,5 @@ use crate::models::algebraic::AlgebraicEquationsOverGF2; +use crate::models::algebraic::Bounded; use crate::models::graph::{MaximumClique, MaximumIndependentSet}; use crate::models::set::ExactCoverBy3Sets; use crate::parameters::ParameterRelation; @@ -237,7 +238,7 @@ fn newly_exact_parameters_match_reduced_instances() { &["num_nonzeros"], exact, ); - check_reduced_parameters::<_, ILP>( + check_reduced_parameters::<_, ILP>( ClosestString::new(2, vec![vec![0, 1], vec![1, 0]]), &["num_nonzeros"], exact, @@ -257,12 +258,12 @@ fn newly_exact_parameters_match_reduced_instances() { &["num_nonzeros"], exact, ); - check_reduced_parameters::<_, ILP>( + check_reduced_parameters::<_, ILP>( FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]), &["num_nonzeros"], exact, ); - check_reduced_parameters::<_, ILP>( + check_reduced_parameters::<_, ILP>( IntegerKnapsack::new(vec![3, 4], vec![5, 6], 7).unwrap(), &["num_nonzeros"], exact, @@ -287,7 +288,7 @@ fn newly_exact_parameters_match_reduced_instances() { &["num_nonzeros"], exact, ); - check_reduced_parameters::<_, ILP>( + check_reduced_parameters::<_, ILP>( RegisterSufficiency::new(4, vec![(2, 0), (3, 1)], 2), &["num_nonzeros"], exact, @@ -318,7 +319,7 @@ fn newly_exact_parameters_match_reduced_instances() { exact, ); check_reduced_parameters::<_, ILP>( - ILP::::with_variables( + ILP::::with_variables( vec![IntegerVariable::new(Some(0), Some(3)).unwrap()], vec![LinearConstraint::le(vec![(0, 1)], 2)], vec![], @@ -382,7 +383,7 @@ fn exact_parameter_formulas_cover_sparse_and_boundary_instances() { &["num_nonzeros"], exact, ); - check_reduced_parameters::<_, ILP>( + check_reduced_parameters::<_, ILP>( RegisterSufficiency::new(0, vec![], 0), &["num_nonzeros"], exact, diff --git a/tests/suites/register_assignment_reductions.rs b/tests/suites/register_assignment_reductions.rs index 04afcc570..61b98c6bc 100644 --- a/tests/suites/register_assignment_reductions.rs +++ b/tests/suites/register_assignment_reductions.rs @@ -1,4 +1,4 @@ -use problemreductions::models::algebraic::ILP; +use problemreductions::models::algebraic::{Bounded, ILP}; use problemreductions::models::formula::{CNFClause, KSatisfiability}; use problemreductions::models::misc::FeasibleRegisterAssignment; use problemreductions::prelude::*; @@ -21,7 +21,7 @@ fn ksat_to_fra_path() -> ReductionPath { fn fra_to_ilp_path() -> ReductionPath { let graph = ReductionGraph::new(); let src = ReductionGraph::variant_to_map(&FeasibleRegisterAssignment::variant()); - let dst = ReductionGraph::variant_to_map(&ILP::::variant()); + let dst = ReductionGraph::variant_to_map(&ILP::::variant()); graph .find_all_paths("FeasibleRegisterAssignment", &src, "ILP", &dst) .into_iter() @@ -64,7 +64,7 @@ fn test_ksat_to_fra_structure_and_closed_loop_via_ilp() { .reduce_along_path(&fra_path, fra as &dyn std::any::Any) .expect("FRA -> ILP reduction should not fail") .expect("FRA -> ILP reduction should execute"); - let ilp = fra_chain.target_problem::>(); + let ilp = fra_chain.target_problem::>(); let ilp_solution = ILPSolver::new() .solve(ilp) @@ -99,7 +99,7 @@ fn test_unsatisfiable_ksat_stays_infeasible_through_fra_to_ilp() { assert!( ILPSolver::new() - .solve(fra_chain.target_problem::>()) + .solve(fra_chain.target_problem::>()) .is_err(), "unsatisfiable source instance should yield an infeasible ILP" ); From 393ae03332bacf0f7843b3f23536ffd327133cf4 Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Sun, 27 Sep 2026 10:05:48 -0700 Subject: [PATCH 2/5] Update paper example selectors for ILP bounds variants --- docs/paper/reductions.typ | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 85c8670e9..adff1e79f 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -5000,7 +5000,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| } #{ - let x = load-model-example("ILP") + let x = load-model-example("ILP", variant: (bounds: "general", coefficient: "i64", variable: "i64")) let nv = x.instance.variables.len() let obj = x.instance.objective let constraints = x.instance.constraints @@ -12621,13 +12621,13 @@ where $P$ is a penalty weight large enough that any constraint violation costs m "QUBO", "ILP", source-variant: (weight: "f64"), - target-variant: (coefficient: "f64", variable: "bool"), + target-variant: (bounds: "general", coefficient: "f64", variable: "bool"), ) #let qubo_ilp_sol = qubo_ilp.solutions.at(0) #reduction-rule("QUBO", "ILP", example: true, example-source-variant: (weight: "f64"), - example-target-variant: (coefficient: "f64", variable: "bool"), + example-target-variant: (bounds: "general", coefficient: "f64", variable: "bool"), example-caption: [4-variable QUBO with 3 quadratic terms], extra: [ #pred-commands( @@ -13456,13 +13456,13 @@ The following reductions to Integer Linear Programming are straightforward formu "MaximumCoKPlex", "ILP", source-variant: (graph: "SimpleGraph", k: "KN", weight: "i64"), - target-variant: (coefficient: "i64", variable: "bool"), + target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), ) #let mckp_ilp_sol = mckp_ilp.solutions.at(0) #reduction-rule("MaximumCoKPlex", "ILP", example: true, example-source-variant: (graph: "SimpleGraph", k: "KN", weight: "i64"), - example-target-variant: (coefficient: "i64", variable: "bool"), + example-target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), example-caption: [Weighted 5-cycle ($n = 5$), $k = 2$], extra: [ #pred-commands( @@ -13491,12 +13491,12 @@ The following reductions to Integer Linear Programming are straightforward formu #let mces_ilp = load-example( "MaximumCommonEdgeSubgraph", "ILP", - target-variant: (coefficient: "i64", variable: "bool"), + target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), ) #let mces_ilp_sol = mces_ilp.solutions.at(0) #reduction-rule("MaximumCommonEdgeSubgraph", "ILP", example: true, - example-target-variant: (coefficient: "i64", variable: "bool"), + example-target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), example-caption: [Two labelled 3-vertex digraphs with 2 arcs each], extra: [ #pred-commands( @@ -13529,12 +13529,12 @@ The following reductions to Integer Linear Programming are straightforward formu #let cmo_ilp = load-example( "MaximumContactMapOverlap", "ILP", - target-variant: (coefficient: "i64", variable: "bool"), + target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), ) #let cmo_ilp_sol = cmo_ilp.solutions.at(0) #reduction-rule("MaximumContactMapOverlap", "ILP", example: true, - example-target-variant: (coefficient: "i64", variable: "bool"), + example-target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), example-caption: [$|V_1| = #cmo_ilp.source.instance.num_vertices_1$, $|E_1| = #cmo_ilp.source.instance.contacts_1.len()$, $|V_2| = #cmo_ilp.source.instance.num_vertices_2$, $|E_2| = #cmo_ilp.source.instance.contacts_2.len()$], extra: [ #pred-commands( @@ -13568,13 +13568,13 @@ The following reductions to Integer Linear Programming are straightforward formu "MaximumEdgeWeightedKClique", "ILP", source-variant: (weight: "i64"), - target-variant: (coefficient: "i64", variable: "bool"), + target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), ) #let mewkc_ilp_sol = mewkc_ilp.solutions.at(0) #reduction-rule("MaximumEdgeWeightedKClique", "ILP", example: true, example-source-variant: (weight: "i64"), - example-target-variant: (coefficient: "i64", variable: "bool"), + example-target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), example-caption: [$n = 4$ vertices, $m = 5$ edges, $k = 3$], extra: [ #pred-commands( @@ -14085,12 +14085,12 @@ The following reductions to Integer Linear Programming are straightforward formu #let cs_ilp_str = load-example( "ClosestString", "ILP", - target-variant: (coefficient: "i64", variable: "i64"), + target-variant: (bounds: "bounded", coefficient: "i64", variable: "i64"), ) #let cs_ilp_str_sol = cs_ilp_str.solutions.at(0) #reduction-rule("ClosestString", "ILP", example: true, - example-target-variant: (coefficient: "i64", variable: "i64"), + example-target-variant: (bounds: "bounded", coefficient: "i64", variable: "i64"), example-caption: [Binary alphabet, 4 length-3 strings], extra: [ #pred-commands( @@ -14128,12 +14128,12 @@ The following reductions to Integer Linear Programming are straightforward formu #let css_ilp = load-example( "ClosestSubstring", "ILP", - target-variant: (coefficient: "i64", variable: "i64"), + target-variant: (bounds: "bounded", coefficient: "i64", variable: "i64"), ) #let css_ilp_sol = css_ilp.solutions.at(0) #reduction-rule("ClosestSubstring", "ILP", example: true, - example-target-variant: (coefficient: "i64", variable: "i64"), + example-target-variant: (bounds: "bounded", coefficient: "i64", variable: "i64"), example-caption: [Binary alphabet, 3 length-5 strings, length-3 windows], extra: [ #pred-commands( @@ -15508,12 +15508,12 @@ The following reductions to Integer Linear Programming are straightforward formu #let hcd_ilp = load-example( "HighlyConnectedDeletion", "ILP", - target-variant: (coefficient: "i64", variable: "bool"), + target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), ) #let hcd_ilp_sol = hcd_ilp.solutions.at(0) #reduction-rule("HighlyConnectedDeletion", "ILP", example: true, - example-target-variant: (coefficient: "i64", variable: "bool"), + example-target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), example-caption: [Triangle plus pendant: $n = 4$ vertices, $m = 4$ edges], extra: [ #pred-commands( @@ -15542,12 +15542,12 @@ The following reductions to Integer Linear Programming are straightforward formu #let ep_ilp = load-example( "EulerianPath", "ILP", - target-variant: (coefficient: "i64", variable: "i64"), + target-variant: (bounds: "bounded", coefficient: "i64", variable: "i64"), ) #let ep_ilp_sol = ep_ilp.solutions.at(0) #reduction-rule("EulerianPath", "ILP", example: true, - example-target-variant: (coefficient: "i64", variable: "i64"), + example-target-variant: (bounds: "bounded", coefficient: "i64", variable: "i64"), example-caption: [3-vertex digraph with 4 arcs (parallel edges)], extra: [ #pred-commands( From a08626f34fbf4884df86c51c8e96fcfa42f3a29f Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:13:11 -0700 Subject: [PATCH 3/5] Bound ILP reduction overhead using intrinsic numeric magnitudes Propagate magnitude bounds through incoming reductions, normalize irrelevant thresholds, and restore composable size predictions with explicit rule-local contracts. Add magnitude parameters for BinPacking, Partition, and SubsetSum and verify composed reductions through QUBO. --- docs/src/design.md | 6 + problemreductions-cli/src/test_support.rs | 4 + src/models/algebraic/ilp.rs | 26 ++ src/models/misc/bin_packing.rs | 16 +- src/models/misc/partition.rs | 10 +- src/models/misc/subset_sum.rs | 13 +- src/rules/acyclicpartition_ilp.rs | 3 + .../balancedcompletebipartitesubgraph_ilp.rs | 1 + src/rules/biconnectivityaugmentation_ilp.rs | 15 +- src/rules/binpacking_ilp.rs | 2 + src/rules/bmf_ilp.rs | 1 + src/rules/bottlenecktravelingsalesman_ilp.rs | 1 + .../boundedcomponentspanningforest_ilp.rs | 3 + src/rules/capacityassignment_ilp.rs | 3 + src/rules/circuit_ilp.rs | 1 + src/rules/closeststring_ilp.rs | 11 +- src/rules/closestsubstring_ilp.rs | 1 + src/rules/clustering_ilp.rs | 1 + src/rules/coloring_ilp.rs | 11 +- src/rules/consecutiveblockminimization_ilp.rs | 21 +- .../consecutiveonesmatrixaugmentation_ilp.rs | 8 +- src/rules/consecutiveonessubmatrix_ilp.rs | 1 + ...onsistencyofdatabasefrequencytables_ilp.rs | 11 +- src/rules/directedhamiltonianpath_ilp.rs | 1 + .../directedtwocommodityintegralflow_ilp.rs | 29 +- src/rules/disjointconnectingpaths_ilp.rs | 1 + src/rules/ensemblecomputation_ilp.rs | 1 + src/rules/eulerianpath_ilp.rs | 1 + src/rules/exactcoverby3sets_ilp.rs | 11 +- src/rules/expectedretrievalcost_ilp.rs | 11 +- src/rules/factoring_ilp.rs | 1 + src/rules/feasibleregisterassignment_ilp.rs | 11 +- src/rules/flowshopscheduling_ilp.rs | 15 +- src/rules/graphpartitioning_ilp.rs | 1 + src/rules/hamiltonianpath_ilp.rs | 1 + src/rules/highlyconnecteddeletion_ilp.rs | 1 + src/rules/ilp_bool_ilp_i64.rs | 1 + src/rules/ilp_bounded_ilp.rs | 1 + src/rules/ilp_casts.rs | 2 + src/rules/ilp_helpers.rs | 12 + src/rules/ilp_i64_ilp_bool.rs | 18 +- src/rules/ilp_qubo.rs | 10 +- src/rules/integerknapsack_ilp.rs | 15 +- src/rules/integralflowbundles_ilp.rs | 3 + src/rules/integralflowhomologousarcs_ilp.rs | 21 +- src/rules/integralflowwithmultipliers_ilp.rs | 3 + src/rules/isomorphicspanningtree_ilp.rs | 1 + src/rules/kclique_ilp.rs | 1 + src/rules/knapsack_ilp.rs | 14 +- src/rules/ksatisfiability_subsetsum.rs | 9 +- src/rules/lengthboundeddisjointpaths_ilp.rs | 19 +- src/rules/longestcircuit_ilp.rs | 4 + src/rules/longestcommonsubsequence_ilp.rs | 1 + src/rules/longestpath_ilp.rs | 1 + src/rules/maximalis_ilp.rs | 1 + src/rules/maximum2satisfiability_ilp.rs | 1 + src/rules/maximumclique_ilp.rs | 1 + src/rules/maximumcokplex_ilp.rs | 13 +- src/rules/maximumcommonedgesubgraph_ilp.rs | 1 + src/rules/maximumcontactmapoverlap_ilp.rs | 11 +- src/rules/maximumdomaticnumber_ilp.rs | 12 +- src/rules/maximumedgeweightedkclique_ilp.rs | 2 + src/rules/maximumleafspanningtree_ilp.rs | 1 + src/rules/maximumlikelihoodranking_ilp.rs | 1 + src/rules/maximummatching_ilp.rs | 1 + src/rules/maximumsetpacking_ilp.rs | 17 +- .../minimumcapacitatedspanningtree_ilp.rs | 15 +- src/rules/minimumcoveringbycliques_ilp.rs | 1 + src/rules/minimumcutintoboundedsets_ilp.rs | 4 +- src/rules/minimumdominatingset_ilp.rs | 1 + src/rules/minimumedgecostflow_ilp.rs | 9 +- ...minimumexternalmacrodatacompression_ilp.rs | 1 + src/rules/minimumfaultdetectiontestset_ilp.rs | 1 + src/rules/minimumfeedbackarcset_ilp.rs | 1 + src/rules/minimumfeedbackvertexset_ilp.rs | 1 + src/rules/minimumgraphbandwidth_ilp.rs | 1 + src/rules/minimumhittingset_ilp.rs | 1 + ...minimuminternalmacrodatacompression_ilp.rs | 1 + src/rules/minimummatrixcover_ilp.rs | 1 + src/rules/minimummaximalmatching_ilp.rs | 1 + src/rules/minimummetricdimension_ilp.rs | 1 + src/rules/minimummultiwaycut_ilp.rs | 1 + src/rules/minimumsetcovering_ilp.rs | 1 + src/rules/minimumsummulticenter_ilp.rs | 1 + src/rules/minimumtardinesssequencing_ilp.rs | 6 + src/rules/minimumweightdecoding_ilp.rs | 1 + src/rules/minmaxmulticenter_ilp.rs | 3 + src/rules/mixedchinesepostman_ilp.rs | 1 + src/rules/monochromatictriangle_ilp.rs | 1 + src/rules/multiplechoicebranching_ilp.rs | 3 + src/rules/multiplecopyfileallocation_ilp.rs | 1 + src/rules/multiprocessorscheduling_ilp.rs | 3 + src/rules/naesatisfiability_ilp.rs | 1 + .../numericalmatchingwithtargetsums_ilp.rs | 1 + src/rules/openshopscheduling_ilp.rs | 9 +- src/rules/optimallineararrangement_ilp.rs | 1 + .../optimumcommunicationspanningtree_ilp.rs | 1 + src/rules/paintshop_ilp.rs | 1 + src/rules/partiallyorderedknapsack_ilp.rs | 3 + src/rules/partition_binpacking.rs | 11 +- src/rules/partition_subsetsum.rs | 2 + src/rules/partitionintocliques_ilp.rs | 1 + src/rules/partitionintopathsoflength2_ilp.rs | 1 + src/rules/partitionintotriangles_ilp.rs | 1 + src/rules/pathconstrainednetworkflow_ilp.rs | 21 +- .../precedenceconstrainedscheduling_ilp.rs | 8 +- src/rules/preemptivescheduling_ilp.rs | 1 + src/rules/quadraticassignment_ilp.rs | 11 +- src/rules/qubo_ilp.rs | 1 + .../rectilinearpicturecompression_ilp.rs | 19 +- src/rules/registersufficiency_ilp.rs | 9 +- .../resourceconstrainedscheduling_ilp.rs | 3 + src/rules/rootedtreestorageassignment_ilp.rs | 20 +- src/rules/ruralpostman_ilp.rs | 1 + ...ingtominimizeweightedcompletiontime_ilp.rs | 3 + .../schedulingwithindividualdeadlines_ilp.rs | 7 +- ...cingtominimizemaximumcumulativecost_ilp.rs | 3 + ...sequencingtominimizetardytaskweight_ilp.rs | 3 + ...ingtominimizeweightedcompletiontime_ilp.rs | 3 + ...quencingtominimizeweightedtardiness_ilp.rs | 15 +- ...equencingwithdeadlinesandsetuptimes_ilp.rs | 15 +- src/rules/sequencingwithinintervals_ilp.rs | 1 + ...uencingwithreleasetimesanddeadlines_ilp.rs | 1 + src/rules/setsplitting_ilp.rs | 6 +- src/rules/shortestcommonsupersequence_ilp.rs | 1 + .../shortestweightconstrainedpath_ilp.rs | 3 + src/rules/sparsematrixcompression_ilp.rs | 1 + src/rules/stackercrane_ilp.rs | 1 + src/rules/steinertree_ilp.rs | 1 + src/rules/stringtostringcorrection_ilp.rs | 1 + .../strongconnectivityaugmentation_ilp.rs | 15 +- src/rules/subgraphisomorphism_ilp.rs | 1 + src/rules/subsetsum_partition.rs | 2 + src/rules/sumofsquarespartition_ilp.rs | 9 +- src/rules/threedimensionalmatching_ilp.rs | 1 + src/rules/timetabledesign_ilp.rs | 20 +- src/rules/travelingsalesman_ilp.rs | 1 + src/rules/undirectedflowlowerbounds_ilp.rs | 15 +- .../undirectedtwocommodityintegralflow_ilp.rs | 3 + src/types.rs | 35 +++ src/unit_tests/models/algebraic/ilp.rs | 98 +++++++ src/unit_tests/models/misc/bin_packing.rs | 34 +++ src/unit_tests/models/misc/partition.rs | 17 ++ src/unit_tests/models/misc/subset_sum.rs | 19 ++ src/unit_tests/reduction_graph.rs | 264 ++++++++++++++++++ .../rules/consecutiveblockminimization_ilp.rs | 26 ++ .../consecutiveonesmatrixaugmentation_ilp.rs | 19 ++ .../directedtwocommodityintegralflow_ilp.rs | 29 ++ src/unit_tests/rules/ilp_qubo.rs | 14 + src/unit_tests/rules/integerknapsack_ilp.rs | 26 ++ .../rules/integralflowhomologousarcs_ilp.rs | 27 ++ src/unit_tests/rules/knapsack_ilp.rs | 23 ++ .../rules/lengthboundeddisjointpaths_ilp.rs | 16 ++ src/unit_tests/rules/maximumcokplex_ilp.rs | 16 ++ .../rules/maximumdomaticnumber_ilp.rs | 15 + src/unit_tests/rules/maximumsetpacking_ilp.rs | 15 + .../rules/minimumcutintoboundedsets_ilp.rs | 26 ++ .../rules/minimumedgecostflow_ilp.rs | 30 ++ .../rules/openshopscheduling_ilp.rs | 27 ++ .../rules/pathconstrainednetworkflow_ilp.rs | 27 ++ .../precedenceconstrainedscheduling_ilp.rs | 20 ++ .../rectilinearpicturecompression_ilp.rs | 27 ++ .../rules/rootedtreestorageassignment_ilp.rs | 27 ++ .../schedulingwithindividualdeadlines_ilp.rs | 24 ++ src/unit_tests/rules/setsplitting_ilp.rs | 29 ++ src/unit_tests/rules/timetabledesign_ilp.rs | 31 ++ .../symbolic_parameter_contracts.rs | 37 ++- 167 files changed, 1579 insertions(+), 193 deletions(-) diff --git a/docs/src/design.md b/docs/src/design.md index e63a6ac7a..0b231ee58 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -485,6 +485,12 @@ nonzeros. If a rule predicts those dimensions by source expressions `f` and `g`, explicitly declare `num_nonzeros <= f * g`. Such structural bounds remain valid when coefficients cancel; exact sparsity can still require additional source information. +Prefer coarse, sound bounds that use existing source parameters. Before adding a +parameter, check whether an equivalent normalization can remove irrelevant input +magnitudes. New parameters must describe intrinsic source data independently of +any reduction, and their propagation must be audited on incoming rules. Keep +model-specific definitions and rule-specific formulas beside their implementations. + `ReductionParameterDeclarations::fields` stores `(name, relation, expression)` triples. Use `ParameterTransform::relation(field)` to inspect a formula's accuracy and `unavailable(field)` for a composition failure and its upstream cause. The uniform diff --git a/problemreductions-cli/src/test_support.rs b/problemreductions-cli/src/test_support.rs index aaaeaf946..b8ff5dd97 100644 --- a/problemreductions-cli/src/test_support.rs +++ b/problemreductions-cli/src/test_support.rs @@ -433,6 +433,10 @@ problemreductions::inventory::submit! { fields: vec![], unavailable: vec![ + problemreductions::rules::registry::UnavailableParameterField { + field: "max_constraint_magnitude_bits", + reason: "the synthetic aggregate-to-ILP reduction has no parameter model", + }, problemreductions::rules::registry::UnavailableParameterField { field: "num_vars", reason: "the synthetic aggregate-to-ILP reduction has no parameter model", diff --git a/src/models/algebraic/ilp.rs b/src/models/algebraic/ilp.rs index 3c61cfeac..13c5a64c7 100644 --- a/src/models/algebraic/ilp.rs +++ b/src/models/algebraic/ilp.rs @@ -484,6 +484,28 @@ impl ILP { .sum() } + /// Smallest `h >= 1` for which every constraint coefficient, RHS and finite + /// variable endpoint has magnitude strictly below `2^h`. + /// + /// Measures normalized feasible-set data, excluding the objective. Infinite + /// endpoints are not numbers in this maximum; this parameter does not + /// certify boundedness. For floating coefficients it measures magnitude, + /// not mantissa precision. + pub fn max_constraint_magnitude_bits(&self) -> u64 { + let row_bits = + crate::types::max_numeric_magnitude_bits(self.constraints.iter().flat_map(|row| { + std::iter::once(row.rhs) + .chain(row.terms.iter().map(|&(_, coefficient)| coefficient)) + })); + let endpoint_bits = crate::types::max_numeric_magnitude_bits( + self.variables + .iter() + .flat_map(|variable| [variable.lower_bound, variable.upper_bound]) + .flatten(), + ); + row_bits.max(endpoint_bits) + } + /// Evaluate the objective in the coefficient domain. pub fn evaluate_objective(&self, values: &[i64]) -> Result { self.objective @@ -628,6 +650,10 @@ impl Problem for ILP; crate::problem_parameters![ + ( + "max_constraint_magnitude_bits", + max_constraint_magnitude_bits + ), ("num_constraints", num_constraints), ("num_nonzeros", num_nonzeros), ("num_vars", num_vars), diff --git a/src/models/misc/bin_packing.rs b/src/models/misc/bin_packing.rs index 0f952f549..994c064b8 100644 --- a/src/models/misc/bin_packing.rs +++ b/src/models/misc/bin_packing.rs @@ -104,6 +104,17 @@ impl BinPacking { pub fn num_items(&self) -> usize { self.sizes.len() } + + /// Smallest h >= 1 bounding the magnitudes of item sizes and capacity by 2^h + /// (strictly). For floating weights, this measures magnitude, not precision. + pub fn max_numeric_magnitude_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.sizes + .iter() + .chain(std::iter::once(&self.capacity)) + .map(W::to_sum), + ) + } } impl Problem for BinPacking @@ -115,7 +126,10 @@ where type Solution = Vec; type Value = Min; - crate::problem_parameters![("num_items", num_items),]; + crate::problem_parameters![ + ("max_numeric_magnitude_bits", max_numeric_magnitude_bits), + ("num_items", num_items), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] diff --git a/src/models/misc/partition.rs b/src/models/misc/partition.rs index 032f4eccc..5e5a36470 100644 --- a/src/models/misc/partition.rs +++ b/src/models/misc/partition.rs @@ -81,6 +81,11 @@ impl Partition { self.sizes.len() } + /// Smallest h >= 1 such that every input size is strictly below 2^h. + pub fn max_numeric_magnitude_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits(self.sizes.iter().copied()) + } + /// Returns the total sum of all sizes. pub fn total_sum(&self) -> i64 { self.sizes.iter().sum() @@ -107,7 +112,10 @@ impl Problem for Partition { type Solution = Vec; type Value = crate::types::Or; - crate::problem_parameters![("num_elements", num_elements),]; + crate::problem_parameters![ + ("max_numeric_magnitude_bits", max_numeric_magnitude_bits), + ("num_elements", num_elements), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/subset_sum.rs b/src/models/misc/subset_sum.rs index deead9112..c2f300359 100644 --- a/src/models/misc/subset_sum.rs +++ b/src/models/misc/subset_sum.rs @@ -131,6 +131,14 @@ impl SubsetSum { pub fn num_elements(&self) -> usize { self.sizes.len() } + + /// Smallest h >= 1 such that every input size and the target are below 2^h. + pub fn max_numeric_magnitude_bits(&self) -> u64 { + self.sizes + .iter() + .chain(std::iter::once(&self.target)) + .fold(1, |bits, value| bits.max(value.bits())) + } } impl Problem for SubsetSum { @@ -138,7 +146,10 @@ impl Problem for SubsetSum { type Solution = Vec; type Value = crate::types::Or; - crate::problem_parameters![("num_elements", num_elements),]; + crate::problem_parameters![ + ("max_numeric_magnitude_bits", max_numeric_magnitude_bits), + ("num_elements", num_elements), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/rules/acyclicpartition_ilp.rs b/src/rules/acyclicpartition_ilp.rs index 27d5c54a0..a176bdca9 100644 --- a/src/rules/acyclicpartition_ilp.rs +++ b/src/rules/acyclicpartition_ilp.rs @@ -44,6 +44,9 @@ impl ReductionResult for ReductionAcyclicPartitionToILP { impl crate::rules::AggregateReductionResult for ReductionAcyclicPartitionToILP {} #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "vertex weights, arc costs and feasibility budgets are not registered source parameters", + }, exact { num_vars = "num_vertices * num_vertices + num_arcs * num_vertices + num_arcs + num_vertices", num_constraints = "num_vertices^2 + 4 * num_vertices + 3 * num_arcs * num_vertices + 2 * num_arcs + 1", diff --git a/src/rules/balancedcompletebipartitesubgraph_ilp.rs b/src/rules/balancedcompletebipartitesubgraph_ilp.rs index a14d79e83..bd1ffb04c 100644 --- a/src/rules/balancedcompletebipartitesubgraph_ilp.rs +++ b/src/rules/balancedcompletebipartitesubgraph_ilp.rs @@ -46,6 +46,7 @@ impl ReductionResult for ReductionBCBSToILP { impl crate::rules::AggregateReductionResult for ReductionBCBSToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "k + 1", num_vars = "num_vertices", num_constraints = "num_vertices^2 + 2", num_nonzeros = "num_vertices * (num_vertices^2 + 2)", diff --git a/src/rules/biconnectivityaugmentation_ilp.rs b/src/rules/biconnectivityaugmentation_ilp.rs index 287d8a603..5a90ee7b5 100644 --- a/src/rules/biconnectivityaugmentation_ilp.rs +++ b/src/rules/biconnectivityaugmentation_ilp.rs @@ -75,11 +75,16 @@ impl ReductionResult for ReductionBiconnAugToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionBiconnAugToILP {} -#[reduction(transform = upper_bound { - num_vars = "num_potential_edges + 2 * num_vertices * (num_vertices + 1) * (num_edges + num_potential_edges)", - num_constraints = "1 + num_vertices * (num_vertices + 1) * (2 * num_edges + 4 * num_potential_edges + num_vertices)", - num_nonzeros = "(num_potential_edges + 2 * num_vertices * (num_vertices + 1) * (num_edges + num_potential_edges)) * (1 + num_vertices * (num_vertices + 1) * (2 * num_edges + 4 * num_potential_edges + num_vertices))", -})] +#[reduction( + transform = upper_bound { + num_vars = "num_potential_edges + 2 * num_vertices * (num_vertices + 1) * (num_edges + num_potential_edges)", + num_constraints = "1 + num_vertices * (num_vertices + 1) * (2 * num_edges + 4 * num_potential_edges + num_vertices)", + num_nonzeros = "(num_potential_edges + 2 * num_vertices * (num_vertices + 1) * (num_edges + num_potential_edges)) * (1 + num_vertices * (num_vertices + 1) * (2 * num_edges + 4 * num_potential_edges + num_vertices))", + }, + unavailable = { + max_constraint_magnitude_bits = "candidate edge weights and the budget are not registered source parameters", + }, +)] impl ReduceTo> for BiconnectivityAugmentation { type Result = ReductionBiconnAugToILP; diff --git a/src/rules/binpacking_ilp.rs b/src/rules/binpacking_ilp.rs index bef93355a..37657bc44 100644 --- a/src/rules/binpacking_ilp.rs +++ b/src/rules/binpacking_ilp.rs @@ -47,12 +47,14 @@ impl ReductionResult for ReductionBPToILP { } } +// Rows copy item sizes and capacity (up to sign). Binary endpoints require only one bit. #[reduction(transform = { exact { num_vars = "num_items * num_items + num_items", num_constraints = "2 * num_items", }, upper_bound { + max_constraint_magnitude_bits = "max_numeric_magnitude_bits", num_nonzeros = "(num_items * num_items + num_items) * (2 * num_items)", }, })] diff --git a/src/rules/bmf_ilp.rs b/src/rules/bmf_ilp.rs index c69887bb6..2d36b0180 100644 --- a/src/rules/bmf_ilp.rs +++ b/src/rules/bmf_ilp.rs @@ -52,6 +52,7 @@ impl ReductionResult for ReductionBMFToILP { #[reduction( transform = exact { + max_constraint_magnitude_bits = "1", num_vars = "rows * rank + rank * cols + rows * rank * cols + rows * cols", num_constraints = "3 * rows * rank * cols + rank * rows * cols + rows * cols + rows * cols", num_nonzeros = "10 * rows * rank * cols + 2 * rows * cols", diff --git a/src/rules/bottlenecktravelingsalesman_ilp.rs b/src/rules/bottlenecktravelingsalesman_ilp.rs index 48925ed95..9845c359f 100644 --- a/src/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/rules/bottlenecktravelingsalesman_ilp.rs @@ -85,6 +85,7 @@ impl ReductionBTSPToILP { num_constraints = "num_vertices^2 + 6 * num_edges * num_vertices + 4 * num_edges + 3 * num_vertices + 1", }, upper_bound { + max_constraint_magnitude_bits = "2", num_nonzeros = "(num_vertices^2 + 2 * num_edges * num_vertices + num_edges) * (num_vertices^2 + 6 * num_edges * num_vertices + 4 * num_edges + 3 * num_vertices + 1)", }, })] diff --git a/src/rules/boundedcomponentspanningforest_ilp.rs b/src/rules/boundedcomponentspanningforest_ilp.rs index d64e72aac..9b5989b8a 100644 --- a/src/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/rules/boundedcomponentspanningforest_ilp.rs @@ -46,6 +46,9 @@ impl ReductionResult for ReductionBCSFToILP { impl crate::rules::AggregateReductionResult for ReductionBCSFToILP {} #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "component weight bounds and vertex weights are not registered source parameters", + }, exact { num_vars = "3 * num_vertices * max_components + 2 * max_components + 2 * num_edges * max_components", num_constraints = "num_vertices + 5 * max_components + 6 * num_vertices * max_components + 6 * num_edges * max_components", diff --git a/src/rules/capacityassignment_ilp.rs b/src/rules/capacityassignment_ilp.rs index f38073a82..3f7bb842c 100644 --- a/src/rules/capacityassignment_ilp.rs +++ b/src/rules/capacityassignment_ilp.rs @@ -50,6 +50,9 @@ impl ReductionResult for ReductionCAToILP { } #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "delays and the delay budget are not registered source parameters", + }, exact { num_vars = "num_links * num_capacities", num_constraints = "num_links + 1", diff --git a/src/rules/circuit_ilp.rs b/src/rules/circuit_ilp.rs index aa7169b5a..31fcb461f 100644 --- a/src/rules/circuit_ilp.rs +++ b/src/rules/circuit_ilp.rs @@ -195,6 +195,7 @@ impl ILPBuilder { impl crate::rules::AggregateReductionResult for ReductionCircuitToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "num_expression_nodes + num_assignment_outputs + 2", num_vars = "num_variables + 2 * num_expression_nodes", num_constraints = "5 * num_expression_nodes + num_assignment_outputs", num_nonzeros = "(num_variables + 2 * num_expression_nodes) * (5 * num_expression_nodes + num_assignment_outputs)", diff --git a/src/rules/closeststring_ilp.rs b/src/rules/closeststring_ilp.rs index 7d2ecbb6c..ad007d5c9 100644 --- a/src/rules/closeststring_ilp.rs +++ b/src/rules/closeststring_ilp.rs @@ -78,13 +78,16 @@ impl ReductionResult for ReductionClosestStringToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "alphabet_size * string_length + 1", num_constraints = "string_length + num_strings", num_nonzeros = "alphabet_size * string_length + num_strings * (string_length + 1)", - } -)] + }, + upper_bound { + max_constraint_magnitude_bits = "string_length + 1", + }, +})] impl ReduceTo> for ClosestString { type Result = ReductionClosestStringToILP; diff --git a/src/rules/closestsubstring_ilp.rs b/src/rules/closestsubstring_ilp.rs index 8328ece0a..c6c3353cf 100644 --- a/src/rules/closestsubstring_ilp.rs +++ b/src/rules/closestsubstring_ilp.rs @@ -124,6 +124,7 @@ fn decode_one_hot( num_constraints = "substring_length + num_strings + total_num_windows + 1", }, upper_bound { + max_constraint_magnitude_bits = "substring_length + 1", num_nonzeros = "(alphabet_size * substring_length + total_num_windows + 1) * (substring_length + num_strings + total_num_windows + 1)", }, })] diff --git a/src/rules/clustering_ilp.rs b/src/rules/clustering_ilp.rs index 95a457064..0a10f254e 100644 --- a/src/rules/clustering_ilp.rs +++ b/src/rules/clustering_ilp.rs @@ -50,6 +50,7 @@ impl ReductionResult for ReductionClusteringToILP { impl crate::rules::AggregateReductionResult for ReductionClusteringToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "num_elements * num_clusters", num_constraints = "num_elements + num_elements * (num_elements - 1) / 2 * num_clusters", num_nonzeros = "(num_elements * num_clusters) * (num_elements + num_elements * (num_elements - 1) / 2 * num_clusters)", diff --git a/src/rules/coloring_ilp.rs b/src/rules/coloring_ilp.rs index 6dfcf29cc..7383ec766 100644 --- a/src/rules/coloring_ilp.rs +++ b/src/rules/coloring_ilp.rs @@ -113,13 +113,16 @@ impl crate::rules::Aggregate } // Register only the KN variant in the reduction graph -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices * num_colors", num_constraints = "num_vertices + num_edges * num_colors", num_nonzeros = "num_colors * (num_vertices + 2 * num_edges)", - } -)] + }, + upper_bound { + max_constraint_magnitude_bits = "2", + }, +})] impl ReduceTo> for KColoring { type Result = ReductionKColoringToILP; diff --git a/src/rules/consecutiveblockminimization_ilp.rs b/src/rules/consecutiveblockminimization_ilp.rs index 08eb9974e..14d1ca199 100644 --- a/src/rules/consecutiveblockminimization_ilp.rs +++ b/src/rules/consecutiveblockminimization_ilp.rs @@ -42,11 +42,14 @@ impl ReductionResult for ReductionCBMToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionCBMToILP {} -#[reduction(transform = upper_bound { - num_vars = "num_cols * num_cols + num_rows * num_cols + num_rows * num_cols", - num_constraints = "num_cols + num_cols + num_rows * num_cols + num_rows + num_rows * num_cols + 1", - num_nonzeros = "(num_cols * num_cols + num_rows * num_cols + num_rows * num_cols) * (num_cols + num_cols + num_rows * num_cols + num_rows + num_rows * num_cols + 1)", -})] +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "num_rows * num_cols + 1", + num_vars = "num_cols * num_cols + num_rows * num_cols + num_rows * num_cols", + num_constraints = "num_cols + num_cols + num_rows * num_cols + num_rows + num_rows * num_cols + 1", + num_nonzeros = "(num_cols * num_cols + num_rows * num_cols + num_rows * num_cols) * (num_cols + num_cols + num_rows * num_cols + num_rows + num_rows * num_cols + 1)", + }, +)] impl ReduceTo> for ConsecutiveBlockMinimization { type Result = ReductionCBMToILP; @@ -115,7 +118,13 @@ impl ReduceTo> for ConsecutiveBlockMinimization { bound_terms.push((b_offset + r * n + p, 1)); } } - constraints.push(LinearConstraint::le(bound_terms, self.bound())); + // The sum is nonnegative and at most the number of Boolean indicators. + // All negative thresholds are equivalent to -1 (infeasible). + let max_blocks = Self::exact_i64(bound_terms.len(), "encoding the block-count bound")?; + constraints.push(LinearConstraint::le( + bound_terms, + self.bound().clamp(-1, max_blocks), + )); let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; diff --git a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs index 18dbd6535..da906ea83 100644 --- a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs +++ b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs @@ -49,6 +49,7 @@ impl crate::rules::AggregateReductionResult for ReductionCOMAToILP {} num_constraints = "num_cols + num_cols + num_rows * num_cols + 2 * num_rows + num_rows + 3 * num_rows * num_cols + 4 * num_rows * num_cols + 1", }, upper_bound { + max_constraint_magnitude_bits = "num_rows * num_cols + num_cols + 1", num_nonzeros = "(num_cols * num_cols + 5 * num_rows * num_cols) * (num_cols + num_cols + num_rows * num_cols + 2 * num_rows + num_rows + 3 * num_rows * num_cols + 4 * num_rows * num_cols + 1)", }, })] @@ -181,7 +182,12 @@ impl ReduceTo> for ConsecutiveOnesMatrixAugmentation { budget_terms.push((f_off + r * n + p, 1)); } } - constraints.push(LinearConstraint::le(budget_terms, self.bound())); + let max_augmentations = + Self::exact_i64(budget_terms.len(), "encoding the augmentation budget")?; + constraints.push(LinearConstraint::le( + budget_terms, + self.bound().min(max_augmentations), + )); let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; diff --git a/src/rules/consecutiveonessubmatrix_ilp.rs b/src/rules/consecutiveonessubmatrix_ilp.rs index 04b40ce82..62b4c3517 100644 --- a/src/rules/consecutiveonessubmatrix_ilp.rs +++ b/src/rules/consecutiveonessubmatrix_ilp.rs @@ -47,6 +47,7 @@ impl ReductionResult for ReductionCOSToILP { impl crate::rules::AggregateReductionResult for ReductionCOSToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "bound + num_cols + 1", num_vars = "num_cols + num_cols * bound + 5 * num_rows * bound", num_constraints = "2 + num_cols + bound + 3 * num_rows + 8 * num_rows * bound", num_nonzeros = "(num_cols + num_cols * bound + 5 * num_rows * bound) * (2 + num_cols + bound + 3 * num_rows + 8 * num_rows * bound)", diff --git a/src/rules/consistencyofdatabasefrequencytables_ilp.rs b/src/rules/consistencyofdatabasefrequencytables_ilp.rs index 0e1853f84..f53094e6a 100644 --- a/src/rules/consistencyofdatabasefrequencytables_ilp.rs +++ b/src/rules/consistencyofdatabasefrequencytables_ilp.rs @@ -135,13 +135,16 @@ impl ReductionResult for ReductionCDFTToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionCDFTToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_objects * total_domain_size + num_objects * num_frequency_cells", num_constraints = "num_objects * num_attributes + num_known_values + num_frequency_cells + 3 * num_objects * num_frequency_cells", num_nonzeros = "num_objects * total_domain_size + num_known_values + 8 * num_objects * num_frequency_cells", - } -)] + }, + upper_bound { + max_constraint_magnitude_bits = "num_objects + 1", + }, +})] impl ReduceTo> for ConsistencyOfDatabaseFrequencyTables { type Result = ReductionCDFTToILP; diff --git a/src/rules/directedhamiltonianpath_ilp.rs b/src/rules/directedhamiltonianpath_ilp.rs index fb4415956..9aa92ae52 100644 --- a/src/rules/directedhamiltonianpath_ilp.rs +++ b/src/rules/directedhamiltonianpath_ilp.rs @@ -54,6 +54,7 @@ impl ReductionResult for ReductionDirectedHamiltonianPathToILP { impl crate::rules::AggregateReductionResult for ReductionDirectedHamiltonianPathToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "num_vertices^2", num_constraints = "3 * num_vertices + num_vertices^3", num_nonzeros = "(num_vertices^2) * (3 * num_vertices + num_vertices^3)", diff --git a/src/rules/directedtwocommodityintegralflow_ilp.rs b/src/rules/directedtwocommodityintegralflow_ilp.rs index 1c2028996..f281c8e22 100644 --- a/src/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/rules/directedtwocommodityintegralflow_ilp.rs @@ -55,11 +55,14 @@ impl ReductionResult for ReductionD2CIFToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionD2CIFToILP {} -#[reduction(transform = upper_bound { - num_vars = "2 * num_arcs", - num_constraints = "num_arcs + 2 * num_vertices + 2", - num_nonzeros = "(2 * num_arcs) * (num_arcs + 2 * num_vertices + 2)", -})] +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "max_capacity * (num_arcs + 1) + 2", + num_vars = "2 * num_arcs", + num_constraints = "num_arcs + 2 * num_vertices + 2", + num_nonzeros = "(2 * num_arcs) * (num_arcs + 2 * num_vertices + 2)", + }, +)] impl ReduceTo> for DirectedTwoCommodityIntegralFlow { type Result = ReductionD2CIFToILP; @@ -136,7 +139,13 @@ impl ReduceTo> for DirectedTwoCommodityIntegralFlow { sink1_terms.push((f1(a), -1)); } } - constraints.push(LinearConstraint::ge(sink1_terms, self.requirement_1())); + constraints.push(LinearConstraint::ge( + sink1_terms, + crate::rules::ilp_helpers::bounded_flow_requirement( + self.requirement_1(), + self.capacities().iter().copied(), + ), + )); // Net flow into sink_2 ≥ requirement_2 let sink_2 = self.sink_2(); @@ -149,7 +158,13 @@ impl ReduceTo> for DirectedTwoCommodityIntegralFlow { sink2_terms.push((f2(a), -1)); } } - constraints.push(LinearConstraint::ge(sink2_terms, self.requirement_2())); + constraints.push(LinearConstraint::ge( + sink2_terms, + crate::rules::ilp_helpers::bounded_flow_requirement( + self.requirement_2(), + self.capacities().iter().copied(), + ), + )); let variables = self .capacities() diff --git a/src/rules/disjointconnectingpaths_ilp.rs b/src/rules/disjointconnectingpaths_ilp.rs index 674db108b..39ddfbfc5 100644 --- a/src/rules/disjointconnectingpaths_ilp.rs +++ b/src/rules/disjointconnectingpaths_ilp.rs @@ -99,6 +99,7 @@ impl crate::rules::AggregateReductionResult for ReductionDCPToILP {} num_constraints = "num_pairs * num_vertices + num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "1", num_nonzeros = "(num_pairs * 2 * num_edges) * (num_pairs * num_vertices + num_vertices)", }, })] diff --git a/src/rules/ensemblecomputation_ilp.rs b/src/rules/ensemblecomputation_ilp.rs index 520017694..ef8a079df 100644 --- a/src/rules/ensemblecomputation_ilp.rs +++ b/src/rules/ensemblecomputation_ilp.rs @@ -86,6 +86,7 @@ impl ReductionResult for ReductionEnsembleComputationToILP { num_constraints = "5 * budget - 1 + budget * (budget - 1) * (1 + 3 * universe_size) + 2 * budget * universe_size + num_subsets * budget * (universe_size + 2) + num_subsets", }, upper_bound { + max_constraint_magnitude_bits = "universe_size + budget + 1", num_nonzeros = "(3 * budget * universe_size + budget * (budget - 1) * (universe_size + 1) + num_subsets * budget + budget) * (5 * budget - 1 + budget * (budget - 1) * (1 + 3 * universe_size) + 2 * budget * universe_size + num_subsets * budget * (universe_size + 2) + num_subsets)", }, })] diff --git a/src/rules/eulerianpath_ilp.rs b/src/rules/eulerianpath_ilp.rs index eb8374704..367b7efba 100644 --- a/src/rules/eulerianpath_ilp.rs +++ b/src/rules/eulerianpath_ilp.rs @@ -148,6 +148,7 @@ fn compatible_pairs(arcs: &[(usize, usize)]) -> Vec<(usize, usize)> { impl crate::rules::AggregateReductionResult for ReductionEulerianPathToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "num_arcs + 1", num_vars = "3 * num_arcs + num_arcs * num_arcs", num_constraints = "5 * num_arcs + 2 * num_arcs * num_arcs + 2", num_nonzeros = "(3 * num_arcs + num_arcs * num_arcs) * (5 * num_arcs + 2 * num_arcs * num_arcs + 2)", diff --git a/src/rules/exactcoverby3sets_ilp.rs b/src/rules/exactcoverby3sets_ilp.rs index 446243f8e..3072e4ea9 100644 --- a/src/rules/exactcoverby3sets_ilp.rs +++ b/src/rules/exactcoverby3sets_ilp.rs @@ -39,13 +39,16 @@ impl ReductionResult for ReductionX3CToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionX3CToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_subsets", num_constraints = "universe_size + 1", num_nonzeros = "4 * num_subsets", - } -)] + }, + upper_bound { + max_constraint_magnitude_bits = "universe_size + 1", + }, +})] impl ReduceTo> for ExactCoverBy3Sets { type Result = ReductionX3CToILP; diff --git a/src/rules/expectedretrievalcost_ilp.rs b/src/rules/expectedretrievalcost_ilp.rs index 0d321f552..5f0d49bad 100644 --- a/src/rules/expectedretrievalcost_ilp.rs +++ b/src/rules/expectedretrievalcost_ilp.rs @@ -76,13 +76,16 @@ impl ReductionResult for ReductionERCToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_records * num_sectors + num_records^2 * num_sectors^2", num_constraints = "num_records + 3 * num_records^2 * num_sectors^2", num_nonzeros = "7 * num_records^2 * num_sectors^2", - } -)] + }, + upper_bound { + max_constraint_magnitude_bits = "2", + }, +})] impl ReduceTo> for ExpectedRetrievalCost { type Result = ReductionERCToILP; diff --git a/src/rules/factoring_ilp.rs b/src/rules/factoring_ilp.rs index c1d6f9316..d3cabd998 100644 --- a/src/rules/factoring_ilp.rs +++ b/src/rules/factoring_ilp.rs @@ -114,6 +114,7 @@ impl ReductionResult for ReductionFactoringToILP { impl crate::rules::AggregateReductionResult for ReductionFactoringToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "num_bits_first + num_bits_second + 2", num_vars = "num_bits_first * num_bits_second + 2 * num_bits_first + 2 * num_bits_second + target_bits", num_constraints = "3 * num_bits_first * num_bits_second + 4 * num_bits_first + 4 * num_bits_second + 3 * target_bits + 1", num_nonzeros = "(num_bits_first * num_bits_second + 2 * num_bits_first + 2 * num_bits_second + target_bits) * (3 * num_bits_first * num_bits_second + 4 * num_bits_first + 4 * num_bits_second + 3 * target_bits + 1)", diff --git a/src/rules/feasibleregisterassignment_ilp.rs b/src/rules/feasibleregisterassignment_ilp.rs index bc2482e47..d5815f763 100644 --- a/src/rules/feasibleregisterassignment_ilp.rs +++ b/src/rules/feasibleregisterassignment_ilp.rs @@ -47,13 +47,16 @@ impl ReductionResult for ReductionFeasibleRegisterAssignmentToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionFeasibleRegisterAssignmentToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "2 * num_vertices + num_vertices * (num_vertices - 1) / 2", num_constraints = "3 * num_vertices * (num_vertices - 1) / 2 + 3 * num_vertices + 2 * num_arcs + 2 * num_same_register_pairs", num_nonzeros = "4 * num_vertices + 4 * num_arcs + 7 * num_vertices * (num_vertices - 1) / 2 + 6 * num_same_register_pairs", - } -)] + }, + upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", + }, +})] impl ReduceTo> for FeasibleRegisterAssignment { type Result = ReductionFeasibleRegisterAssignmentToILP; diff --git a/src/rules/flowshopscheduling_ilp.rs b/src/rules/flowshopscheduling_ilp.rs index 7bffe8cea..affc67e2f 100644 --- a/src/rules/flowshopscheduling_ilp.rs +++ b/src/rules/flowshopscheduling_ilp.rs @@ -71,11 +71,16 @@ impl ReductionResult for ReductionFSSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionFSSToILP {} -#[reduction(transform = upper_bound { - num_vars = "num_jobs * (num_jobs - 1) / 2 + num_jobs * num_processors", - num_constraints = "num_jobs * (num_jobs - 1) + num_jobs + num_jobs * (num_processors - 1) + num_jobs * (num_jobs - 1) * num_processors + num_jobs", - num_nonzeros = "(num_jobs * (num_jobs - 1) / 2 + num_jobs * num_processors) * (num_jobs * (num_jobs - 1) + num_jobs + num_jobs * (num_processors - 1) + num_jobs * (num_jobs - 1) * num_processors + num_jobs)", -})] +#[reduction( + transform = upper_bound { + num_vars = "num_jobs * (num_jobs - 1) / 2 + num_jobs * num_processors", + num_constraints = "num_jobs * (num_jobs - 1) + num_jobs + num_jobs * (num_processors - 1) + num_jobs * (num_jobs - 1) * num_processors + num_jobs", + num_nonzeros = "(num_jobs * (num_jobs - 1) / 2 + num_jobs * num_processors) * (num_jobs * (num_jobs - 1) + num_jobs + num_jobs * (num_processors - 1) + num_jobs * (num_jobs - 1) * num_processors + num_jobs)", + }, + unavailable = { + max_constraint_magnitude_bits = "processing times and the deadline are not registered source parameters", + }, +)] impl ReduceTo> for FlowShopScheduling { type Result = ReductionFSSToILP; diff --git a/src/rules/graphpartitioning_ilp.rs b/src/rules/graphpartitioning_ilp.rs index d6cceea7a..4965ccedd 100644 --- a/src/rules/graphpartitioning_ilp.rs +++ b/src/rules/graphpartitioning_ilp.rs @@ -49,6 +49,7 @@ impl ReductionResult for ReductionGraphPartitioningToILP { num_constraints = "2 * num_edges + 1", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 2", num_nonzeros = "(num_vertices + num_edges) * (2 * num_edges + 1)", }, })] diff --git a/src/rules/hamiltonianpath_ilp.rs b/src/rules/hamiltonianpath_ilp.rs index c5087553c..d6ba3a77c 100644 --- a/src/rules/hamiltonianpath_ilp.rs +++ b/src/rules/hamiltonianpath_ilp.rs @@ -55,6 +55,7 @@ impl crate::rules::AggregateReductionResult for ReductionHamiltonianPathToILP {} #[reduction( transform = exact { + max_constraint_magnitude_bits = "1", num_vars = "num_vertices^2 + 2 * num_edges * num_consecutive_positions", num_constraints = "2 * num_vertices + 6 * num_edges * num_consecutive_positions + num_consecutive_positions", num_nonzeros = "2 * num_vertices^2 + 16 * num_edges * num_consecutive_positions", diff --git a/src/rules/highlyconnecteddeletion_ilp.rs b/src/rules/highlyconnecteddeletion_ilp.rs index 7d5d9f556..bceaf7be6 100644 --- a/src/rules/highlyconnecteddeletion_ilp.rs +++ b/src/rules/highlyconnecteddeletion_ilp.rs @@ -155,6 +155,7 @@ fn enumerate_feasible_clusters( #[reduction( transform = exact { + max_constraint_magnitude_bits = "1", num_constraints = "num_vertices", }, unavailable = { diff --git a/src/rules/ilp_bool_ilp_i64.rs b/src/rules/ilp_bool_ilp_i64.rs index e62303aeb..8a5c9b55e 100644 --- a/src/rules/ilp_bool_ilp_i64.rs +++ b/src/rules/ilp_bool_ilp_i64.rs @@ -34,6 +34,7 @@ impl ReductionResult for ReductionBinaryILPToIntILP { #[reduction( transform = exact { + max_constraint_magnitude_bits = "max_constraint_magnitude_bits", num_vars = "num_vars", num_constraints = "num_constraints", num_nonzeros = "num_nonzeros", diff --git a/src/rules/ilp_bounded_ilp.rs b/src/rules/ilp_bounded_ilp.rs index 5c4679171..26d57103f 100644 --- a/src/rules/ilp_bounded_ilp.rs +++ b/src/rules/ilp_bounded_ilp.rs @@ -33,6 +33,7 @@ impl ReductionResult for ReductionBoundedILPToILP { impl crate::rules::AggregateReductionResult for ReductionBoundedILPToILP {} #[reduction(transform = exact { + max_constraint_magnitude_bits = "max_constraint_magnitude_bits", num_vars = "num_vars", num_constraints = "num_constraints", num_nonzeros = "num_nonzeros", diff --git a/src/rules/ilp_casts.rs b/src/rules/ilp_casts.rs index 586fe6605..088ce6dca 100644 --- a/src/rules/ilp_casts.rs +++ b/src/rules/ilp_casts.rs @@ -77,6 +77,7 @@ impl ReductionResult for ReductionILPToFloat { #[reduction( transform = exact { + max_constraint_magnitude_bits = "max_constraint_magnitude_bits", num_vars = "num_vars", num_constraints = "num_constraints", num_nonzeros = "num_nonzeros", @@ -92,6 +93,7 @@ impl ReduceTo> for ILP { #[reduction( transform = exact { + max_constraint_magnitude_bits = "max_constraint_magnitude_bits", num_vars = "num_vars", num_constraints = "num_constraints", num_nonzeros = "num_nonzeros", diff --git a/src/rules/ilp_helpers.rs b/src/rules/ilp_helpers.rs index 4f982bdc5..22bf6e298 100644 --- a/src/rules/ilp_helpers.rs +++ b/src/rules/ilp_helpers.rs @@ -2,6 +2,18 @@ use crate::models::algebraic::LinearConstraint; +/// Normalize a lower threshold for flow in `[-sum(capacities), sum(capacities)]`. +/// Capacities must be nonnegative. Requests above the range remain infeasible; +/// those below it remain redundant. Saturation is safe because the input +/// threshold is i64, so it cannot exceed a larger mathematical range. +pub(crate) fn bounded_flow_requirement( + requirement: i64, + capacities: impl IntoIterator, +) -> i64 { + let magnitude = capacities.into_iter().fold(0_i64, i64::saturating_add); + requirement.clamp(-magnitude, magnitude.saturating_add(1)) +} + /// Convert exact ILP integer values into a source model's `usize` representation. pub fn decode_usize_values(values: &[i64]) -> crate::rules::ExtractionResult> { values diff --git a/src/rules/ilp_i64_ilp_bool.rs b/src/rules/ilp_i64_ilp_bool.rs index 055a0f450..4d357920d 100644 --- a/src/rules/ilp_i64_ilp_bool.rs +++ b/src/rules/ilp_i64_ilp_bool.rs @@ -110,13 +110,19 @@ impl ReductionResult for ReductionIntILPToBinaryILP { } } +// If all finite endpoints and row entries have magnitude below 2^h, widths +// need at most h+1 bits. Encoded coefficients are below 2^(2h+1), and the +// lower-bound shift gives |b'| < 2^h + n*2^(2h) < 2^(2h+n+1). #[reduction( - transform = exact { - num_constraints = "num_constraints", - }, - unavailable = { - num_vars = "the binary width depends on concrete variable bounds, not registered problem parameters", - num_nonzeros = "binary expansion depends on concrete variable bounds and row sparsity", + transform = { + exact { + num_constraints = "num_constraints", + }, + upper_bound { + num_vars = "num_vars * (max_constraint_magnitude_bits + 1)", + num_nonzeros = "num_nonzeros * (max_constraint_magnitude_bits + 1)", + max_constraint_magnitude_bits = "2 * max_constraint_magnitude_bits + num_vars + 1", + }, }, )] impl ReduceTo> for ILP { diff --git a/src/rules/ilp_qubo.rs b/src/rules/ilp_qubo.rs index 32408abe6..68d37f8c9 100644 --- a/src/rules/ilp_qubo.rs +++ b/src/rules/ilp_qubo.rs @@ -78,12 +78,12 @@ impl crate::rules::AggregateReductionResult for ReductionILPToQUBO { } } -// Each successfully computed positive i64 slack range needs at most 63 bits. -// Distinct off-diagonal pairs bound the resulting quadratic terms, including -// when penalty contributions cancel. Both bounds use only source parameters. +// With n variables and magnitude bits h, each positive slack range is less +// than (n+1)*2^h <= 2^(n+h), so each row adds at most n+h bits. Squaring the +// resulting variable bound covers every off-diagonal pair, even with cancellation. #[reduction(transform = upper_bound { - num_vars = "num_vars + 63 * num_constraints", - num_quadratic_terms = "(num_vars + 63 * num_constraints) * (num_vars + 63 * num_constraints - 1) / 2", + num_vars = "num_vars + num_constraints * (num_vars + max_constraint_magnitude_bits)", + num_quadratic_terms = "(num_vars + num_constraints * (num_vars + max_constraint_magnitude_bits))^2", })] impl ReduceTo> for ILP { type Result = ReductionILPToQUBO; diff --git a/src/rules/integerknapsack_ilp.rs b/src/rules/integerknapsack_ilp.rs index b22c6ec50..5ee902be1 100644 --- a/src/rules/integerknapsack_ilp.rs +++ b/src/rules/integerknapsack_ilp.rs @@ -1,7 +1,7 @@ //! Reduction from IntegerKnapsack to `ILP`. //! //! Each item multiplicity becomes a non-negative integer ILP variable. The -//! capacity inequality is kept directly, and explicit upper bounds +//! capacity inequality omits oversized items (whose multiplicities are zero), and explicit upper bounds //! `c_i <= floor(B / s_i)` preserve the exact witness domain of the source. use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; @@ -32,13 +32,16 @@ impl ReductionResult for ReductionIntegerKnapsackToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_items", num_constraints = "num_items + 1", + }, + upper_bound { + max_constraint_magnitude_bits = "capacity + 1", num_nonzeros = "2 * num_items", - } -)] + }, +})] impl ReduceTo> for IntegerKnapsack { type Result = ReductionIntegerKnapsackToILP; @@ -52,6 +55,8 @@ impl ReduceTo> for IntegerKnapsack { sizes .iter() .enumerate() + // Oversized items already have multiplicity fixed to zero by their bounds. + .filter(|&(_, &size)| size <= self.capacity()) .map(|(item, &size)| (item, size)) .collect(), self.capacity(), diff --git a/src/rules/integralflowbundles_ilp.rs b/src/rules/integralflowbundles_ilp.rs index 9bac3603b..224233008 100644 --- a/src/rules/integralflowbundles_ilp.rs +++ b/src/rules/integralflowbundles_ilp.rs @@ -42,6 +42,9 @@ impl ReductionResult for ReductionIFBToILP { impl crate::rules::AggregateReductionResult for ReductionIFBToILP {} #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "bundle capacities and the flow requirement are not registered source parameters", + }, exact { num_vars = "num_arcs", num_constraints = "num_bundles + num_vertices - 1", diff --git a/src/rules/integralflowhomologousarcs_ilp.rs b/src/rules/integralflowhomologousarcs_ilp.rs index 7b9db705f..c6c8cf8b4 100644 --- a/src/rules/integralflowhomologousarcs_ilp.rs +++ b/src/rules/integralflowhomologousarcs_ilp.rs @@ -40,11 +40,14 @@ impl ReductionResult for ReductionIFHAToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionIFHAToILP {} -#[reduction(transform = upper_bound { - num_vars = "num_arcs", - num_constraints = "num_arcs^2 + num_arcs + num_vertices + 1", - num_nonzeros = "num_arcs * (num_arcs^2 + num_arcs + num_vertices + 1)", -})] +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "max_capacity * (num_arcs + 1) + 2", + num_vars = "num_arcs", + num_constraints = "num_arcs^2 + num_arcs + num_vertices + 1", + num_nonzeros = "num_arcs * (num_arcs^2 + num_arcs + num_vertices + 1)", + }, +)] impl ReduceTo> for IntegralFlowHomologousArcs { type Result = ReductionIFHAToILP; @@ -91,7 +94,13 @@ impl ReduceTo> for IntegralFlowHomologousArcs { sink_terms.push((arc_idx, -1)); // outgoing } } - constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + constraints.push(LinearConstraint::ge( + sink_terms, + crate::rules::ilp_helpers::bounded_flow_requirement( + self.requirement(), + self.capacities().iter().copied(), + ), + )); let variables = self .capacities() diff --git a/src/rules/integralflowwithmultipliers_ilp.rs b/src/rules/integralflowwithmultipliers_ilp.rs index 19d042c5f..4da1eb82f 100644 --- a/src/rules/integralflowwithmultipliers_ilp.rs +++ b/src/rules/integralflowwithmultipliers_ilp.rs @@ -41,6 +41,9 @@ impl ReductionResult for ReductionIFWMToILP { impl crate::rules::AggregateReductionResult for ReductionIFWMToILP {} #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "vertex multipliers are not bounded by registered source parameters", + }, exact { num_vars = "num_arcs", num_constraints = "num_arcs + num_vertices - 1", diff --git a/src/rules/isomorphicspanningtree_ilp.rs b/src/rules/isomorphicspanningtree_ilp.rs index fadee1715..66f7db1fc 100644 --- a/src/rules/isomorphicspanningtree_ilp.rs +++ b/src/rules/isomorphicspanningtree_ilp.rs @@ -43,6 +43,7 @@ impl ReductionResult for ReductionISTToILP { impl crate::rules::AggregateReductionResult for ReductionISTToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "2", num_vars = "num_vertices * num_vertices", num_constraints = "2 * num_vertices + 2 * (num_vertices - 1) * num_vertices * num_vertices", num_nonzeros = "(num_vertices * num_vertices) * (2 * num_vertices + 2 * (num_vertices - 1) * num_vertices * num_vertices)", diff --git a/src/rules/kclique_ilp.rs b/src/rules/kclique_ilp.rs index 0ed5ae82e..34af3cb66 100644 --- a/src/rules/kclique_ilp.rs +++ b/src/rules/kclique_ilp.rs @@ -58,6 +58,7 @@ impl ReductionResult for ReductionKCliqueToILP { impl crate::rules::AggregateReductionResult for ReductionKCliqueToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "k + 1", num_vars = "num_vertices", num_constraints = "num_vertices^2 + 1", num_nonzeros = "num_vertices * (num_vertices^2 + 1)", diff --git a/src/rules/knapsack_ilp.rs b/src/rules/knapsack_ilp.rs index 1505e7a76..280ab2902 100644 --- a/src/rules/knapsack_ilp.rs +++ b/src/rules/knapsack_ilp.rs @@ -4,6 +4,8 @@ //! - Variables: one binary variable per item //! - Constraint: the total selected weight must not exceed capacity //! - Objective: maximize the total selected value +//! +//! Items exceeding capacity are fixed to zero and omitted from the capacity row. use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::Knapsack; @@ -37,9 +39,10 @@ impl ReductionResult for ReductionKnapsackToILP { #[reduction(transform = { exact { num_vars = "num_items", - num_constraints = "1", }, upper_bound { + max_constraint_magnitude_bits = "capacity + 1", + num_constraints = "num_items + 1", num_nonzeros = "num_items * 1", }, })] @@ -51,14 +54,21 @@ impl ReduceTo> for Knapsack { let weights = self.weights(); let values = self.values(); let capacity = self.capacity(); - let constraints = vec![LinearConstraint::le( + let mut constraints = vec![LinearConstraint::le( weights .iter() .enumerate() + .filter(|&(_, &weight)| weight <= capacity) .map(|(item, &weight)| (item, weight)) .collect(), capacity, )]; + // Oversized items are impossible choices; their magnitudes need not enter the ILP. + for (item, &weight) in weights.iter().enumerate() { + if weight > capacity { + constraints.push(LinearConstraint::eq(vec![(item, 1)], 0)); + } + } let objective = values.iter().copied().enumerate().collect(); let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Maximize) .map_err(>>::target_construction)?; diff --git a/src/rules/ksatisfiability_subsetsum.rs b/src/rules/ksatisfiability_subsetsum.rs index 7079a3db8..3efa0a862 100644 --- a/src/rules/ksatisfiability_subsetsum.rs +++ b/src/rules/ksatisfiability_subsetsum.rs @@ -72,9 +72,12 @@ fn digits_to_integer(digits: &[u8]) -> BigUint { #[crate::aggregate_reduction(identity)] impl crate::rules::AggregateReductionResult for Reduction3SATToSubsetSum {} -#[reduction( - transform = upper_bound { num_elements = "2 * num_vars + 2 * num_clauses" } -)] +// Every integer has at most n+m decimal digits, and 10 < 2^4. The extra 1 +// also covers the empty formula's zero target (whose magnitude parameter is 1). +#[reduction(transform = upper_bound { + num_elements = "2 * num_vars + 2 * num_clauses", + max_numeric_magnitude_bits = "4 * (num_vars + num_clauses) + 1", +})] impl ReduceTo for KSatisfiability { type Result = Reduction3SATToSubsetSum; diff --git a/src/rules/lengthboundeddisjointpaths_ilp.rs b/src/rules/lengthboundeddisjointpaths_ilp.rs index 2c330682c..6dc5a2ce3 100644 --- a/src/rules/lengthboundeddisjointpaths_ilp.rs +++ b/src/rules/lengthboundeddisjointpaths_ilp.rs @@ -100,11 +100,14 @@ impl ReductionResult for ReductionLBDPToILP { } } -#[reduction(transform = upper_bound { - num_vars = "max_paths * 2 * num_edges + max_paths", - num_constraints = "max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths", - num_nonzeros = "(max_paths * 2 * num_edges + max_paths) * (max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths)", -})] +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", + num_vars = "max_paths * 2 * num_edges + max_paths", + num_constraints = "max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths", + num_nonzeros = "(max_paths * 2 * num_edges + max_paths) * (max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths)", + }, +)] impl ReduceTo> for LengthBoundedDisjointPaths { type Result = ReductionLBDPToILP; @@ -115,7 +118,11 @@ impl ReduceTo> for LengthBoundedDisjointPaths { let m = edges.len(); let n = self.num_vertices(); let j = self.max_paths(); - let max_len = Self::exact_i64(self.max_length(), "encoding the path-length bound")?; + // A simple path uses at most n-1 edges; cycles never improve the path packing. + let max_len = Self::exact_i64( + self.max_length().min(n.saturating_sub(1)), + "encoding the path-length bound", + )?; let s = self.source(); let t = self.sink(); diff --git a/src/rules/longestcircuit_ilp.rs b/src/rules/longestcircuit_ilp.rs index cbac8605f..489689502 100644 --- a/src/rules/longestcircuit_ilp.rs +++ b/src/rules/longestcircuit_ilp.rs @@ -62,6 +62,7 @@ impl ReductionResult for ReductionLongestCircuitToILP { num_constraints = "2 + num_vertices + 2 * num_vertices^2 + 2 * num_edges * num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "2", num_nonzeros = "(num_edges + 2 * num_vertices + 2 * num_edges * num_vertices) * (2 + num_vertices + 2 * num_vertices^2 + 2 * num_edges * num_vertices)", }, })] @@ -217,6 +218,9 @@ impl crate::rules::AggregateReductionResult for ReductionDecisionLongestCircuitT upper_bound { num_nonzeros = "(num_edges + 2 * num_vertices + 2 * num_edges * num_vertices) * (3 + num_vertices + 2 * num_vertices^2 + 2 * num_edges * num_vertices)", }, + unavailable { + max_constraint_magnitude_bits = "the decision threshold and edge lengths copied into the acceptance row are not registered source parameters", + }, })] impl ReduceTo> for Decision> { type Result = ReductionDecisionLongestCircuitToILP; diff --git a/src/rules/longestcommonsubsequence_ilp.rs b/src/rules/longestcommonsubsequence_ilp.rs index d156ed261..a35ba3a48 100644 --- a/src/rules/longestcommonsubsequence_ilp.rs +++ b/src/rules/longestcommonsubsequence_ilp.rs @@ -51,6 +51,7 @@ impl ReductionResult for ReductionLCSToILP { #[reduction( transform = exact { + max_constraint_magnitude_bits = "1", num_vars = "max_length * (alphabet_size + 1) + max_length * total_length", num_constraints = "max_length + num_transitions + max_length * num_strings + max_length * total_length + num_transitions * sum_triangular_lengths", num_nonzeros = "max_length * (alphabet_size + 1 + num_strings + 3 * total_length) + 2 * num_transitions * (1 + sum_triangular_lengths)", diff --git a/src/rules/longestpath_ilp.rs b/src/rules/longestpath_ilp.rs index da691c818..1fc8e7159 100644 --- a/src/rules/longestpath_ilp.rs +++ b/src/rules/longestpath_ilp.rs @@ -54,6 +54,7 @@ impl ReductionResult for ReductionLongestPathToILP { num_constraints = "5 * num_edges + 4 * num_vertices + 1", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(2 * num_edges + num_vertices) * (5 * num_edges + 4 * num_vertices + 1)", }, })] diff --git a/src/rules/maximalis_ilp.rs b/src/rules/maximalis_ilp.rs index 0830cbd6e..abe3b0782 100644 --- a/src/rules/maximalis_ilp.rs +++ b/src/rules/maximalis_ilp.rs @@ -38,6 +38,7 @@ impl ReductionResult for ReductionMxISToILP { num_constraints = "num_edges + num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "2 * num_edges + 2", num_nonzeros = "num_vertices * (num_edges + num_vertices)", }, })] diff --git a/src/rules/maximum2satisfiability_ilp.rs b/src/rules/maximum2satisfiability_ilp.rs index a9445762b..8dd0d60df 100644 --- a/src/rules/maximum2satisfiability_ilp.rs +++ b/src/rules/maximum2satisfiability_ilp.rs @@ -46,6 +46,7 @@ impl ReductionResult for ReductionMaximum2SatisfiabilityToILP { num_constraints = "num_clauses", }, upper_bound { + max_constraint_magnitude_bits = "2", num_nonzeros = "(num_vars + num_clauses) * num_clauses", }, })] diff --git a/src/rules/maximumclique_ilp.rs b/src/rules/maximumclique_ilp.rs index f4922efd5..752c767fd 100644 --- a/src/rules/maximumclique_ilp.rs +++ b/src/rules/maximumclique_ilp.rs @@ -46,6 +46,7 @@ impl ReductionResult for ReductionCliqueToILP { } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "num_vertices", num_constraints = "num_vertices^2", num_nonzeros = "num_vertices * (num_vertices^2)", diff --git a/src/rules/maximumcokplex_ilp.rs b/src/rules/maximumcokplex_ilp.rs index d9e6067e1..2b1ff1161 100644 --- a/src/rules/maximumcokplex_ilp.rs +++ b/src/rules/maximumcokplex_ilp.rs @@ -44,17 +44,16 @@ where fn build_constraints(graph: &SimpleGraph, bound_k: usize) -> Result, ()> { (0..graph.num_vertices()) .map(|v| { - let degree = i64::try_from(graph.degree(v)).map_err(|_| ())?; - let bound_k = i64::try_from(bound_k).map_err(|_| ())?; + let degree = graph.degree(v); + let allowed_neighbors = + i64::try_from(bound_k.checked_sub(1).ok_or(())?.min(degree)).map_err(|_| ())?; + let degree = i64::try_from(degree).map_err(|_| ())?; let mut terms: Vec<(usize, i64)> = graph.neighbors(v).into_iter().map(|u| (u, 1)).collect(); if degree > 0 { terms.push((v, degree)); } - let rhs = degree - .checked_add(bound_k) - .and_then(|value| value.checked_sub(1)) - .ok_or(())?; + let rhs = degree.checked_add(allowed_neighbors).ok_or(())?; Ok(LinearConstraint::le(terms, rhs)) }) .collect() @@ -86,6 +85,7 @@ where num_constraints = "num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "4 * num_edges + 2", num_nonzeros = "num_vertices * num_vertices", }, })] @@ -115,6 +115,7 @@ impl ReduceTo> for MaximumCoKPlex { num_constraints = "num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "4 * num_edges + 2", num_nonzeros = "num_vertices * num_vertices", }, })] diff --git a/src/rules/maximumcommonedgesubgraph_ilp.rs b/src/rules/maximumcommonedgesubgraph_ilp.rs index 64001e434..4ed3f6e90 100644 --- a/src/rules/maximumcommonedgesubgraph_ilp.rs +++ b/src/rules/maximumcommonedgesubgraph_ilp.rs @@ -67,6 +67,7 @@ impl ReductionResult for ReductionMCESToILP { } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "2", num_vars = "num_vertices_1 * num_vertices_2 + num_arcs_1 * num_arcs_2", num_constraints = "num_vertices_1 + num_vertices_2 + 3 * num_arcs_1 * num_arcs_2", num_nonzeros = "(num_vertices_1 * num_vertices_2 + num_arcs_1 * num_arcs_2) * (num_vertices_1 + num_vertices_2 + 3 * num_arcs_1 * num_arcs_2)", diff --git a/src/rules/maximumcontactmapoverlap_ilp.rs b/src/rules/maximumcontactmapoverlap_ilp.rs index 5970556b6..d134b77e9 100644 --- a/src/rules/maximumcontactmapoverlap_ilp.rs +++ b/src/rules/maximumcontactmapoverlap_ilp.rs @@ -69,13 +69,16 @@ impl ReductionResult for ReductionCMOToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices_1 * num_vertices_2 + num_contacts_1 * num_contacts_2", num_constraints = "num_vertices_1 + num_vertices_2 + num_vertices_1 * (num_vertices_1 - 1) / 2 * num_vertices_2 * (num_vertices_2 + 1) / 2 + 2 * num_contacts_1 * num_contacts_2", num_nonzeros = "2 * num_vertices_1 * num_vertices_2 + num_vertices_1 * (num_vertices_1 - 1) * num_vertices_2 * (num_vertices_2 + 1) / 2 + 4 * num_contacts_1 * num_contacts_2", - } -)] + }, + upper_bound { + max_constraint_magnitude_bits = "2", + }, +})] impl ReduceTo> for MaximumContactMapOverlap { type Result = ReductionCMOToILP; diff --git a/src/rules/maximumdomaticnumber_ilp.rs b/src/rules/maximumdomaticnumber_ilp.rs index acba50a38..312c616ca 100644 --- a/src/rules/maximumdomaticnumber_ilp.rs +++ b/src/rules/maximumdomaticnumber_ilp.rs @@ -60,6 +60,7 @@ impl ReductionResult for ReductionDomaticNumberToILP { #[reduction(transform = { exact { + max_constraint_magnitude_bits = "1", num_vars = "num_vertices * num_vertices + num_vertices", num_constraints = "num_vertices + num_vertices * num_vertices + num_vertices * num_vertices", }, @@ -84,12 +85,13 @@ impl ReduceTo> for MaximumDomaticNumber { // Domination constraints: for each v, i: x_{v,i} + Σ_{u ∈ N(v)} x_{u,i} >= y_i // Rewritten as: x_{v,i} + Σ_{u ∈ N(v)} x_{u,i} - y_i >= 0 for v in 0..n { - let neighbors = self.graph().neighbors(v); + let mut neighborhood = self.graph().neighbors(v); + neighborhood.push(v); + neighborhood.sort_unstable(); + neighborhood.dedup(); for i in 0..n { - let mut terms: Vec<(usize, i64)> = vec![(v * n + i, 1)]; - for &u in &neighbors { - terms.push((u * n + i, 1)); - } + let mut terms: Vec<(usize, i64)> = + neighborhood.iter().map(|&u| (u * n + i, 1)).collect(); // -y_i terms.push((n * n + i, -1)); constraints.push(LinearConstraint::ge(terms, 0)); diff --git a/src/rules/maximumedgeweightedkclique_ilp.rs b/src/rules/maximumedgeweightedkclique_ilp.rs index 58f4cf93d..06ad62928 100644 --- a/src/rules/maximumedgeweightedkclique_ilp.rs +++ b/src/rules/maximumedgeweightedkclique_ilp.rs @@ -144,6 +144,7 @@ where num_constraints = "1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(num_vertices + num_edges) * (1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges)", }, })] @@ -161,6 +162,7 @@ impl ReduceTo> for MaximumEdgeWeightedKClique { num_constraints = "1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(num_vertices + num_edges) * (1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges)", }, })] diff --git a/src/rules/maximumleafspanningtree_ilp.rs b/src/rules/maximumleafspanningtree_ilp.rs index 49c0d8724..e0398c6a9 100644 --- a/src/rules/maximumleafspanningtree_ilp.rs +++ b/src/rules/maximumleafspanningtree_ilp.rs @@ -61,6 +61,7 @@ impl ReductionResult for ReductionMaximumLeafSpanningTreeToILP { num_constraints = "3 * num_vertices + 2 * num_edges + 1", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 2", num_nonzeros = "(3 * num_edges + num_vertices) * (3 * num_vertices + 2 * num_edges + 1)", }, })] diff --git a/src/rules/maximumlikelihoodranking_ilp.rs b/src/rules/maximumlikelihoodranking_ilp.rs index 0afd9524a..14d009bf7 100644 --- a/src/rules/maximumlikelihoodranking_ilp.rs +++ b/src/rules/maximumlikelihoodranking_ilp.rs @@ -74,6 +74,7 @@ impl ReductionResult for ReductionMaximumLikelihoodRankingToILP { #[reduction( transform = exact { + max_constraint_magnitude_bits = "1", num_vars = "num_items * (num_items - 1) / 2", num_constraints = "num_items * (num_items - 1) * (num_items - 2) / 3", num_nonzeros = "num_items * (num_items - 1) * (num_items - 2)", diff --git a/src/rules/maximummatching_ilp.rs b/src/rules/maximummatching_ilp.rs index 84f01cdb4..42f1a3799 100644 --- a/src/rules/maximummatching_ilp.rs +++ b/src/rules/maximummatching_ilp.rs @@ -50,6 +50,7 @@ impl ReductionResult for ReductionMatchingToILP { num_vars = "num_edges", }, upper_bound { + max_constraint_magnitude_bits = "2", num_constraints = "num_vertices", num_nonzeros = "2 * num_edges", }, diff --git a/src/rules/maximumsetpacking_ilp.rs b/src/rules/maximumsetpacking_ilp.rs index be215a8f8..a2ac50e5c 100644 --- a/src/rules/maximumsetpacking_ilp.rs +++ b/src/rules/maximumsetpacking_ilp.rs @@ -39,11 +39,14 @@ impl ReductionResult for ReductionSPToILP { } } -#[reduction(transform = upper_bound { - num_vars = "num_sets", - num_constraints = "universe_size", - num_nonzeros = "num_sets * universe_size", -})] +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "1", + num_vars = "num_sets", + num_constraints = "universe_size", + num_nonzeros = "num_sets * universe_size", + }, +)] impl ReduceTo> for MaximumSetPacking { type Result = ReductionSPToILP; @@ -58,6 +61,10 @@ impl ReduceTo> for MaximumSetPacking { elem_to_sets[e].push(i); } } + // Each set contributes once per element, irrespective of repeated input entries. + for sets in &mut elem_to_sets { + sets.dedup(); + } let constraints: Vec = elem_to_sets .into_iter() diff --git a/src/rules/minimumcapacitatedspanningtree_ilp.rs b/src/rules/minimumcapacitatedspanningtree_ilp.rs index 515521491..2dbf72968 100644 --- a/src/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/rules/minimumcapacitatedspanningtree_ilp.rs @@ -62,11 +62,16 @@ impl ReductionResult for ReductionMinimumCapacitatedSpanningTreeToILP { } } -#[reduction(transform = upper_bound { - num_vars = "5 * num_edges", - num_constraints = "5 * num_edges + 2 * num_vertices + 1", - num_nonzeros = "(5 * num_edges) * (5 * num_edges + 2 * num_vertices + 1)", -})] +#[reduction( + transform = upper_bound { + num_vars = "5 * num_edges", + num_constraints = "5 * num_edges + 2 * num_vertices + 1", + num_nonzeros = "(5 * num_edges) * (5 * num_edges + 2 * num_vertices + 1)", + }, + unavailable = { + max_constraint_magnitude_bits = "capacity and vertex requirements are not registered source parameters", + }, +)] impl ReduceTo> for MinimumCapacitatedSpanningTree { type Result = ReductionMinimumCapacitatedSpanningTreeToILP; diff --git a/src/rules/minimumcoveringbycliques_ilp.rs b/src/rules/minimumcoveringbycliques_ilp.rs index 63278b39d..37bc8a8ff 100644 --- a/src/rules/minimumcoveringbycliques_ilp.rs +++ b/src/rules/minimumcoveringbycliques_ilp.rs @@ -67,6 +67,7 @@ impl ReductionResult for ReductionMinimumCoveringByCliquesToILP { num_constraints = "num_vertices * num_edges + (num_vertices * (num_vertices - 1) / 2 - num_edges) * num_edges + 3 * num_edges * num_edges + num_edges", }, upper_bound { + max_constraint_magnitude_bits = "2", num_nonzeros = "(num_vertices * num_edges + num_edges + num_edges * num_edges) * (num_vertices * num_edges + (num_vertices * (num_vertices - 1) / 2 - num_edges) * num_edges + 3 * num_edges * num_edges + num_edges)", }, })] diff --git a/src/rules/minimumcutintoboundedsets_ilp.rs b/src/rules/minimumcutintoboundedsets_ilp.rs index 0386d9f6a..9b609d32d 100644 --- a/src/rules/minimumcutintoboundedsets_ilp.rs +++ b/src/rules/minimumcutintoboundedsets_ilp.rs @@ -45,6 +45,7 @@ impl ReductionResult for ReductionMinCutBSToILP { num_constraints = "2 + 2 + 2 * num_edges", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(num_vertices + num_edges) * (2 + 2 + 2 * num_edges)", }, })] @@ -57,7 +58,8 @@ impl ReduceTo> for MinimumCutIntoBoundedSets { let m = edges.len(); let num_vars = n + m; let n_i64 = Self::exact_i64(n, "encoding the partition size")?; - let size_bound = Self::exact_i64(self.size_bound(), "encoding the set-size bound")?; + // Each side contains at most all n vertices. + let size_bound = Self::exact_i64(self.size_bound().min(n), "encoding the set-size bound")?; let mut constraints = Vec::new(); // x_s = 0 diff --git a/src/rules/minimumdominatingset_ilp.rs b/src/rules/minimumdominatingset_ilp.rs index 83ad5fb96..2dbbd0823 100644 --- a/src/rules/minimumdominatingset_ilp.rs +++ b/src/rules/minimumdominatingset_ilp.rs @@ -52,6 +52,7 @@ impl ReductionResult for ReductionDSToILP { num_constraints = "num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "2 * num_edges + 2", num_nonzeros = "num_vertices * num_vertices", }, })] diff --git a/src/rules/minimumedgecostflow_ilp.rs b/src/rules/minimumedgecostflow_ilp.rs index 29c3ae0e6..d3c4cc8ac 100644 --- a/src/rules/minimumedgecostflow_ilp.rs +++ b/src/rules/minimumedgecostflow_ilp.rs @@ -59,6 +59,7 @@ impl ReductionResult for ReductionMECFToILP { num_constraints = "2 * num_edges + num_vertices - 1", }, upper_bound { + max_constraint_magnitude_bits = "max_capacity * (num_edges + 1) + 2", num_nonzeros = "(2 * num_edges) * (2 * num_edges + num_vertices - 1)", }, })] @@ -119,7 +120,13 @@ impl ReduceTo> for MinimumEdgeCostFlow { sink_terms.push((f(a), -1)); } } - constraints.push(LinearConstraint::ge(sink_terms, self.required_flow())); + constraints.push(LinearConstraint::ge( + sink_terms, + crate::rules::ilp_helpers::bounded_flow_requirement( + self.required_flow(), + self.capacities().iter().copied(), + ), + )); // Objective: minimize Σ p(a) · y_a let objective: Vec<(usize, i64)> = (0..m).map(|a| (y(a), self.prices()[a])).collect(); diff --git a/src/rules/minimumexternalmacrodatacompression_ilp.rs b/src/rules/minimumexternalmacrodatacompression_ilp.rs index 5f6acb6c8..09e460598 100644 --- a/src/rules/minimumexternalmacrodatacompression_ilp.rs +++ b/src/rules/minimumexternalmacrodatacompression_ilp.rs @@ -214,6 +214,7 @@ fn encode_pointer(n: usize, start: usize, len: usize) -> usize { } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "string_length * alphabet_size + 2 * string_length + string_length ^ 3", num_constraints = "string_length + string_length * alphabet_size + string_length + string_length + 1 + string_length ^ 3 * string_length", num_nonzeros = "(string_length * alphabet_size + 2 * string_length + string_length ^ 3) * (string_length + string_length * alphabet_size + string_length + string_length + 1 + string_length ^ 3 * string_length)", diff --git a/src/rules/minimumfaultdetectiontestset_ilp.rs b/src/rules/minimumfaultdetectiontestset_ilp.rs index c98bc4051..f1795f0b2 100644 --- a/src/rules/minimumfaultdetectiontestset_ilp.rs +++ b/src/rules/minimumfaultdetectiontestset_ilp.rs @@ -49,6 +49,7 @@ impl ReductionResult for ReductionMFDTSToILP { num_constraints = "num_vertices - num_inputs - num_outputs", }, upper_bound { + max_constraint_magnitude_bits = "1", num_nonzeros = "(num_inputs * num_outputs) * (num_vertices - num_inputs - num_outputs)", }, })] diff --git a/src/rules/minimumfeedbackarcset_ilp.rs b/src/rules/minimumfeedbackarcset_ilp.rs index f91edd92a..2cafd52ec 100644 --- a/src/rules/minimumfeedbackarcset_ilp.rs +++ b/src/rules/minimumfeedbackarcset_ilp.rs @@ -60,6 +60,7 @@ impl ReductionResult for ReductionFASToILP { num_constraints = "num_arcs + num_arcs + num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(num_arcs + num_vertices) * (num_arcs + num_arcs + num_vertices)", }, })] diff --git a/src/rules/minimumfeedbackvertexset_ilp.rs b/src/rules/minimumfeedbackvertexset_ilp.rs index e2747758d..554de5e28 100644 --- a/src/rules/minimumfeedbackvertexset_ilp.rs +++ b/src/rules/minimumfeedbackvertexset_ilp.rs @@ -57,6 +57,7 @@ impl ReductionResult for ReductionMFVSToILP { num_constraints = "num_arcs + 2 * num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "2 * num_vertices + 1", num_nonzeros = "(2 * num_vertices) * (num_arcs + 2 * num_vertices)", }, })] diff --git a/src/rules/minimumgraphbandwidth_ilp.rs b/src/rules/minimumgraphbandwidth_ilp.rs index e4ed07ec1..7504bfa1a 100644 --- a/src/rules/minimumgraphbandwidth_ilp.rs +++ b/src/rules/minimumgraphbandwidth_ilp.rs @@ -55,6 +55,7 @@ impl ReductionResult for ReductionMGBToILP { num_constraints = "2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 1 + 2 * num_edges", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(num_vertices^2 + num_vertices + 1) * (2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 1 + 2 * num_edges)", }, })] diff --git a/src/rules/minimumhittingset_ilp.rs b/src/rules/minimumhittingset_ilp.rs index e9771539d..c6eeb8c77 100644 --- a/src/rules/minimumhittingset_ilp.rs +++ b/src/rules/minimumhittingset_ilp.rs @@ -37,6 +37,7 @@ impl ReductionResult for ReductionHSToILP { num_constraints = "num_sets", }, upper_bound { + max_constraint_magnitude_bits = "1", num_nonzeros = "universe_size * num_sets", }, })] diff --git a/src/rules/minimuminternalmacrodatacompression_ilp.rs b/src/rules/minimuminternalmacrodatacompression_ilp.rs index 1c58d96b1..e418a0417 100644 --- a/src/rules/minimuminternalmacrodatacompression_ilp.rs +++ b/src/rules/minimuminternalmacrodatacompression_ilp.rs @@ -160,6 +160,7 @@ impl ReductionResult for ReductionIMDCToILP { } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "string_len + string_len ^ 3", num_constraints = "string_len + 1", num_nonzeros = "(string_len + string_len ^ 3) * (string_len + 1)", diff --git a/src/rules/minimummatrixcover_ilp.rs b/src/rules/minimummatrixcover_ilp.rs index 8de7cad1d..84e7afe6f 100644 --- a/src/rules/minimummatrixcover_ilp.rs +++ b/src/rules/minimummatrixcover_ilp.rs @@ -54,6 +54,7 @@ fn y_index(n: usize, i: usize, j: usize) -> usize { #[reduction( transform = exact { + max_constraint_magnitude_bits = "1", num_vars = "num_rows + num_rows * (num_rows - 1) / 2", num_constraints = "3 * num_rows * (num_rows - 1) / 2", num_nonzeros = "7 * num_rows * (num_rows - 1) / 2", diff --git a/src/rules/minimummaximalmatching_ilp.rs b/src/rules/minimummaximalmatching_ilp.rs index 93dcd0cf3..9d9c1ce98 100644 --- a/src/rules/minimummaximalmatching_ilp.rs +++ b/src/rules/minimummaximalmatching_ilp.rs @@ -54,6 +54,7 @@ impl ReductionResult for ReductionMMMToILP { num_constraints = "num_vertices + num_edges", }, upper_bound { + max_constraint_magnitude_bits = "2", num_nonzeros = "num_edges * (num_vertices + num_edges)", }, })] diff --git a/src/rules/minimummetricdimension_ilp.rs b/src/rules/minimummetricdimension_ilp.rs index 12fef2119..1a71ac3ce 100644 --- a/src/rules/minimummetricdimension_ilp.rs +++ b/src/rules/minimummetricdimension_ilp.rs @@ -54,6 +54,7 @@ impl ReductionResult for ReductionMDToILP { num_constraints = "num_vertices * (num_vertices - 1) / 2", }, upper_bound { + max_constraint_magnitude_bits = "1", num_nonzeros = "num_vertices * (num_vertices * (num_vertices - 1) / 2)", }, })] diff --git a/src/rules/minimummultiwaycut_ilp.rs b/src/rules/minimummultiwaycut_ilp.rs index 4600bf0ef..1986a3586 100644 --- a/src/rules/minimummultiwaycut_ilp.rs +++ b/src/rules/minimummultiwaycut_ilp.rs @@ -63,6 +63,7 @@ impl ReductionResult for ReductionMMCToILP { num_constraints = "num_vertices + 2 * num_terminals * num_edges + num_terminals * num_terminals", }, upper_bound { + max_constraint_magnitude_bits = "1", num_nonzeros = "(num_terminals * num_vertices + num_edges) * (num_vertices + 2 * num_terminals * num_edges + num_terminals * num_terminals)", }, })] diff --git a/src/rules/minimumsetcovering_ilp.rs b/src/rules/minimumsetcovering_ilp.rs index 7b830ac62..2805a7407 100644 --- a/src/rules/minimumsetcovering_ilp.rs +++ b/src/rules/minimumsetcovering_ilp.rs @@ -49,6 +49,7 @@ impl ReductionResult for ReductionSCToILP { num_constraints = "universe_size", }, upper_bound { + max_constraint_magnitude_bits = "1", num_nonzeros = "num_sets * universe_size", }, })] diff --git a/src/rules/minimumsummulticenter_ilp.rs b/src/rules/minimumsummulticenter_ilp.rs index edfa101cd..d2e33f01b 100644 --- a/src/rules/minimumsummulticenter_ilp.rs +++ b/src/rules/minimumsummulticenter_ilp.rs @@ -118,6 +118,7 @@ fn weighted_distances_msmc( } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_vars = "num_vertices + num_vertices^2", num_constraints = "num_vertices^2 + 2 * num_vertices + 1", num_nonzeros = "(num_vertices + num_vertices^2) * (num_vertices^2 + 2 * num_vertices + 1)", diff --git a/src/rules/minimumtardinesssequencing_ilp.rs b/src/rules/minimumtardinesssequencing_ilp.rs index bbf61f745..6a0951d63 100644 --- a/src/rules/minimumtardinesssequencing_ilp.rs +++ b/src/rules/minimumtardinesssequencing_ilp.rs @@ -105,6 +105,9 @@ fn build_common_constraints( // Unit-length variant #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "task deadlines and processing times are not bounded by registered source parameters", + }, exact { num_vars = "num_tasks * num_tasks + num_tasks", num_constraints = "2 * num_tasks + num_precedences + num_tasks", @@ -151,6 +154,9 @@ impl ReduceTo> for MinimumTardinessSequencing { // Arbitrary-length variant #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "task deadlines and processing times are not bounded by registered source parameters", + }, exact { num_vars = "num_tasks * num_tasks + num_tasks", num_constraints = "2 * num_tasks + num_precedences + num_tasks * num_tasks", diff --git a/src/rules/minimumweightdecoding_ilp.rs b/src/rules/minimumweightdecoding_ilp.rs index f6f643d9a..879784922 100644 --- a/src/rules/minimumweightdecoding_ilp.rs +++ b/src/rules/minimumweightdecoding_ilp.rs @@ -59,6 +59,7 @@ impl ReductionResult for ReductionMinimumWeightDecodingToILP { num_constraints = "num_rows + num_cols", }, upper_bound { + max_constraint_magnitude_bits = "num_cols + 2", num_nonzeros = "(num_cols + num_rows) * (num_rows + num_cols)", }, })] diff --git a/src/rules/minmaxmulticenter_ilp.rs b/src/rules/minmaxmulticenter_ilp.rs index a8eff69bc..a60d92f4b 100644 --- a/src/rules/minmaxmulticenter_ilp.rs +++ b/src/rules/minmaxmulticenter_ilp.rs @@ -122,6 +122,9 @@ fn weighted_distances_mmc( } #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "weighted graph distances are not bounded by registered source parameters", + }, exact { num_vars = "num_vertices + num_vertices^2 + 1", num_constraints = "2 * num_vertices^2 + 3 * num_vertices + 2", diff --git a/src/rules/mixedchinesepostman_ilp.rs b/src/rules/mixedchinesepostman_ilp.rs index 5a6b77cb1..0cecfd58d 100644 --- a/src/rules/mixedchinesepostman_ilp.rs +++ b/src/rules/mixedchinesepostman_ilp.rs @@ -43,6 +43,7 @@ impl ReductionResult for ReductionMCPToILP { } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "(num_arcs + num_edges + 1) * (num_vertices + 1) + 1", num_vars = "num_edges + 4 * (num_arcs + 2 * num_edges) + 3 * num_vertices + 1", num_constraints = "num_edges + 8 * (num_arcs + 2 * num_edges) + 10 * num_vertices + 2", num_nonzeros = "(num_edges + 4 * (num_arcs + 2 * num_edges) + 3 * num_vertices + 1) * (num_edges + 8 * (num_arcs + 2 * num_edges) + 10 * num_vertices + 2)", diff --git a/src/rules/monochromatictriangle_ilp.rs b/src/rules/monochromatictriangle_ilp.rs index 5bdcc3449..ddbc7dacd 100644 --- a/src/rules/monochromatictriangle_ilp.rs +++ b/src/rules/monochromatictriangle_ilp.rs @@ -44,6 +44,7 @@ impl ReductionResult for ReductionMonochromaticTriangleToILP { impl crate::rules::AggregateReductionResult for ReductionMonochromaticTriangleToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "2", num_vars = "num_edges", num_constraints = "2 * num_triangles + num_vertices^5 / 8", num_nonzeros = "num_edges * (2 * num_triangles + num_vertices^5 / 8)", diff --git a/src/rules/multiplechoicebranching_ilp.rs b/src/rules/multiplechoicebranching_ilp.rs index 8a63e05fc..1580909ed 100644 --- a/src/rules/multiplechoicebranching_ilp.rs +++ b/src/rules/multiplechoicebranching_ilp.rs @@ -40,6 +40,9 @@ impl ReductionResult for ReductionMultipleChoiceBranchingToILP { impl crate::rules::AggregateReductionResult for ReductionMultipleChoiceBranchingToILP {} #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "arc weights and the feasibility threshold are not registered source parameters", + }, exact { num_vars = "num_arcs + num_vertices", num_constraints = "2 * num_arcs + 2 * num_vertices + num_partition_groups + 1", diff --git a/src/rules/multiplecopyfileallocation_ilp.rs b/src/rules/multiplecopyfileallocation_ilp.rs index 9b294b615..4a67d6a92 100644 --- a/src/rules/multiplecopyfileallocation_ilp.rs +++ b/src/rules/multiplecopyfileallocation_ilp.rs @@ -74,6 +74,7 @@ fn bfs_distances(graph: &SimpleGraph, source: usize, n: usize) -> Vec { num_constraints = "num_vertices^2 + num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "1", num_nonzeros = "(num_vertices + num_vertices^2) * (num_vertices^2 + num_vertices)", }, })] diff --git a/src/rules/multiprocessorscheduling_ilp.rs b/src/rules/multiprocessorscheduling_ilp.rs index be832401b..e38aee57e 100644 --- a/src/rules/multiprocessorscheduling_ilp.rs +++ b/src/rules/multiprocessorscheduling_ilp.rs @@ -57,6 +57,9 @@ impl ReductionResult for ReductionMSToILP { impl crate::rules::AggregateReductionResult for ReductionMSToILP {} #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "task lengths and the deadline are not registered source parameters", + }, exact { num_vars = "num_tasks * num_processors", num_constraints = "num_tasks + num_processors", diff --git a/src/rules/naesatisfiability_ilp.rs b/src/rules/naesatisfiability_ilp.rs index 03d6a3b72..b2218bee2 100644 --- a/src/rules/naesatisfiability_ilp.rs +++ b/src/rules/naesatisfiability_ilp.rs @@ -50,6 +50,7 @@ impl crate::rules::AggregateReductionResult for ReductionNAESATToILP {} num_constraints = "2 * num_clauses", }, upper_bound { + max_constraint_magnitude_bits = "num_literals + 1", num_nonzeros = "num_vars * (2 * num_clauses)", }, })] diff --git a/src/rules/numericalmatchingwithtargetsums_ilp.rs b/src/rules/numericalmatchingwithtargetsums_ilp.rs index 670905866..a063d074a 100644 --- a/src/rules/numericalmatchingwithtargetsums_ilp.rs +++ b/src/rules/numericalmatchingwithtargetsums_ilp.rs @@ -71,6 +71,7 @@ impl ReductionResult for ReductionNMTSToILP { impl crate::rules::AggregateReductionResult for ReductionNMTSToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "num_pairs * num_pairs * num_pairs", num_constraints = "3 * num_pairs", num_nonzeros = "(num_pairs * num_pairs * num_pairs) * (3 * num_pairs)", diff --git a/src/rules/openshopscheduling_ilp.rs b/src/rules/openshopscheduling_ilp.rs index 67819b545..ca6ad3955 100644 --- a/src/rules/openshopscheduling_ilp.rs +++ b/src/rules/openshopscheduling_ilp.rs @@ -109,6 +109,7 @@ impl ReductionResult for ReductionOSSToILP { num_constraints = "num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + 1 + 2 * num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 2 * num_jobs * num_machines * (num_machines - 1) / 2 + num_jobs * num_machines", }, upper_bound { + max_constraint_magnitude_bits = "schedule_horizon + 1", num_nonzeros = "(num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 1) * (num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + 1 + 2 * num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 2 * num_jobs * num_machines * (num_machines - 1) / 2 + num_jobs * num_machines)", }, })] @@ -323,6 +324,7 @@ impl crate::rules::AggregateReductionResult for ReductionDecisionOpenShopSchedul num_constraints = "3 * num_jobs * (num_jobs - 1) / 2 * num_machines + 2 * num_jobs * num_machines + 3 * num_jobs * num_machines * (num_machines - 1) / 2 + 2", }, upper_bound { + max_constraint_magnitude_bits = "schedule_horizon + 1", num_nonzeros = "(num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 1) * (3 * num_jobs * (num_jobs - 1) / 2 * num_machines + 2 * num_jobs * num_machines + 3 * num_jobs * num_machines * (num_machines - 1) / 2 + 2)", }, })] @@ -332,9 +334,14 @@ impl ReduceTo> for Decision { fn reduce_to(&self) -> Result { let mut inner = ReduceTo::>::reduce_to(self.inner())?; let mut constraints = inner.target.constraints().to_vec(); + // The makespan variable is already bounded by the total processing time. + let horizon = >>::exact_i64( + self.inner().schedule_horizon(), + "encoding the makespan bound", + )?; constraints.push(LinearConstraint::le( inner.target.objective().to_vec(), - *self.bound(), + (*self.bound()).clamp(-1, horizon), )); inner.target = ILP::with_variables( inner.target.variables().to_vec(), diff --git a/src/rules/optimallineararrangement_ilp.rs b/src/rules/optimallineararrangement_ilp.rs index dfd67cf74..de1ca2add 100644 --- a/src/rules/optimallineararrangement_ilp.rs +++ b/src/rules/optimallineararrangement_ilp.rs @@ -55,6 +55,7 @@ impl ReductionResult for ReductionOLAToILP { num_constraints = "2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 3 * num_edges", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(num_vertices^2 + num_vertices + num_edges) * (2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 3 * num_edges)", }, })] diff --git a/src/rules/optimumcommunicationspanningtree_ilp.rs b/src/rules/optimumcommunicationspanningtree_ilp.rs index b586d3211..54fc448ec 100644 --- a/src/rules/optimumcommunicationspanningtree_ilp.rs +++ b/src/rules/optimumcommunicationspanningtree_ilp.rs @@ -52,6 +52,7 @@ impl ReductionResult for ReductionOptimumCommunicationSpanningTreeToILP { num_constraints = "1 + num_vertices * num_vertices * (num_vertices - 1) / 2 + 2 * num_edges * num_vertices * (num_vertices - 1) / 2", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(num_edges + 2 * num_edges * num_vertices * (num_vertices - 1) / 2) * (1 + num_vertices * num_vertices * (num_vertices - 1) / 2 + 2 * num_edges * num_vertices * (num_vertices - 1) / 2)", }, })] diff --git a/src/rules/paintshop_ilp.rs b/src/rules/paintshop_ilp.rs index 6e51ae1a3..608538774 100644 --- a/src/rules/paintshop_ilp.rs +++ b/src/rules/paintshop_ilp.rs @@ -38,6 +38,7 @@ impl ReductionResult for ReductionPaintShopToILP { } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "num_cars + 2 * num_sequence", num_constraints = "num_sequence + 2 * num_sequence", num_nonzeros = "(num_cars + 2 * num_sequence) * (num_sequence + 2 * num_sequence)", diff --git a/src/rules/partiallyorderedknapsack_ilp.rs b/src/rules/partiallyorderedknapsack_ilp.rs index 546d20027..b4d0a70e7 100644 --- a/src/rules/partiallyorderedknapsack_ilp.rs +++ b/src/rules/partiallyorderedknapsack_ilp.rs @@ -32,6 +32,9 @@ impl ReductionResult for ReductionPOKToILP { } #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "item weights and capacity are not registered source parameters", + }, exact { num_vars = "num_items", num_constraints = "num_precedences + 1", diff --git a/src/rules/partition_binpacking.rs b/src/rules/partition_binpacking.rs index a5e2872aa..57411e031 100644 --- a/src/rules/partition_binpacking.rs +++ b/src/rules/partition_binpacking.rs @@ -66,10 +66,15 @@ impl crate::rules::AggregateReductionResult for ReductionPartitionToBinPacking { } } -#[reduction( - transform = exact { +// Capacity is at most max(1, sum(sizes)/2); summing n entries adds at most n bits. +#[reduction(transform = { + exact { num_items = "num_elements", - })] + }, + upper_bound { + max_numeric_magnitude_bits = "max_numeric_magnitude_bits + num_elements", + }, +})] impl ReduceTo> for Partition { type Result = ReductionPartitionToBinPacking; diff --git a/src/rules/partition_subsetsum.rs b/src/rules/partition_subsetsum.rs index ceaca59ee..afcac632f 100644 --- a/src/rules/partition_subsetsum.rs +++ b/src/rules/partition_subsetsum.rs @@ -51,8 +51,10 @@ impl ReductionResult for ReductionPartitionToSubsetSum { #[crate::aggregate_reduction(identity)] impl crate::rules::AggregateReductionResult for ReductionPartitionToSubsetSum {} +// The target is half the sum, bounded by n*2^h; odd sums use the constant NO instance. #[reduction( transform = upper_bound { + max_numeric_magnitude_bits = "max_numeric_magnitude_bits + num_elements", num_elements = "num_elements", })] impl ReduceTo for Partition { diff --git a/src/rules/partitionintocliques_ilp.rs b/src/rules/partitionintocliques_ilp.rs index 7ef0c820d..b530b6fbf 100644 --- a/src/rules/partitionintocliques_ilp.rs +++ b/src/rules/partitionintocliques_ilp.rs @@ -50,6 +50,7 @@ impl ReductionResult for ReductionPartitionIntoCliquesToILP { impl crate::rules::AggregateReductionResult for ReductionPartitionIntoCliquesToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "num_vertices^2", num_constraints = "num_vertices + num_vertices^3", num_nonzeros = "(num_vertices^2) * (num_vertices + num_vertices^3)", diff --git a/src/rules/partitionintopathsoflength2_ilp.rs b/src/rules/partitionintopathsoflength2_ilp.rs index 30b32901d..8dea37fd6 100644 --- a/src/rules/partitionintopathsoflength2_ilp.rs +++ b/src/rules/partitionintopathsoflength2_ilp.rs @@ -68,6 +68,7 @@ impl ReductionResult for ReductionPIPL2ToILP { impl crate::rules::AggregateReductionResult for ReductionPIPL2ToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "2", num_vars = "num_vertices^2 + num_edges * num_vertices", num_constraints = "num_vertices^2 + num_edges * num_vertices + num_vertices", num_nonzeros = "(num_vertices^2 + num_edges * num_vertices) * (num_vertices^2 + num_edges * num_vertices + num_vertices)", diff --git a/src/rules/partitionintotriangles_ilp.rs b/src/rules/partitionintotriangles_ilp.rs index 3ec37775c..45dc8be3e 100644 --- a/src/rules/partitionintotriangles_ilp.rs +++ b/src/rules/partitionintotriangles_ilp.rs @@ -61,6 +61,7 @@ impl ReductionResult for ReductionPITToILP { impl crate::rules::AggregateReductionResult for ReductionPITToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "2", num_vars = "num_vertices^2", num_constraints = "num_vertices^2 * num_vertices", num_nonzeros = "(num_vertices^2) * (num_vertices^2 * num_vertices)", diff --git a/src/rules/pathconstrainednetworkflow_ilp.rs b/src/rules/pathconstrainednetworkflow_ilp.rs index 99383217c..7b7c59494 100644 --- a/src/rules/pathconstrainednetworkflow_ilp.rs +++ b/src/rules/pathconstrainednetworkflow_ilp.rs @@ -40,11 +40,14 @@ impl ReductionResult for ReductionPCNFToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionPCNFToILP {} -#[reduction(transform = upper_bound { - num_vars = "num_paths", - num_constraints = "num_arcs + 1", - num_nonzeros = "num_paths * (num_arcs + 1)", -})] +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "max_capacity * (num_paths + 1) + 2", + num_vars = "num_paths", + num_constraints = "num_arcs + 1", + num_nonzeros = "num_paths * (num_arcs + 1)", + }, +)] impl ReduceTo> for PathConstrainedNetworkFlow { type Result = ReductionPCNFToILP; @@ -69,7 +72,13 @@ impl ReduceTo> for PathConstrainedNetworkFlow { // Total flow requirement: sum_i f_i >= R let total_terms: Vec<(usize, i64)> = (0..num_paths).map(|i| (i, 1)).collect(); - constraints.push(LinearConstraint::ge(total_terms, self.requirement())); + constraints.push(LinearConstraint::ge( + total_terms, + crate::rules::ilp_helpers::bounded_flow_requirement( + self.requirement(), + self.paths().iter().map(|path| self.path_bottleneck(path)), + ), + )); let variables = self .paths() diff --git a/src/rules/precedenceconstrainedscheduling_ilp.rs b/src/rules/precedenceconstrainedscheduling_ilp.rs index 5416b8e6d..10cd0e7c3 100644 --- a/src/rules/precedenceconstrainedscheduling_ilp.rs +++ b/src/rules/precedenceconstrainedscheduling_ilp.rs @@ -67,6 +67,7 @@ impl crate::rules::AggregateReductionResult for ReductionPCSToILP {} num_constraints = "num_tasks + deadline + num_precedences", }, upper_bound { + max_constraint_magnitude_bits = "num_tasks + deadline + 1", num_nonzeros = "(num_tasks * deadline) * (num_tasks + deadline + num_precedences)", }, })] @@ -85,8 +86,11 @@ impl ReduceTo> for PrecedenceConstrainedScheduling { // x_{j,t} variable index let var = |j: usize, t: usize| j * d + t; - let processor_count = - Self::exact_i64(self.num_processors(), "encoding the processor capacity")?; + // More processors than tasks cannot admit any additional schedules. + let processor_count = Self::exact_i64( + self.num_processors().min(n), + "encoding the processor capacity", + )?; let mut constraints = Vec::new(); diff --git a/src/rules/preemptivescheduling_ilp.rs b/src/rules/preemptivescheduling_ilp.rs index e66317261..f6e9e438f 100644 --- a/src/rules/preemptivescheduling_ilp.rs +++ b/src/rules/preemptivescheduling_ilp.rs @@ -73,6 +73,7 @@ impl ReductionResult for ReductionPSToILP { num_constraints = "num_tasks + d_max + num_precedences * d_max + 2 * num_tasks * d_max", }, upper_bound { + max_constraint_magnitude_bits = "d_max + num_processors + 1", num_nonzeros = "(num_tasks * d_max + 1) * (num_tasks + d_max + num_precedences * d_max + 2 * num_tasks * d_max)", }, })] diff --git a/src/rules/quadraticassignment_ilp.rs b/src/rules/quadraticassignment_ilp.rs index f1585eb7d..157bf6937 100644 --- a/src/rules/quadraticassignment_ilp.rs +++ b/src/rules/quadraticassignment_ilp.rs @@ -49,13 +49,16 @@ impl ReductionResult for ReductionQAPToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_facilities * num_locations + num_facilities * (num_facilities - 1) * num_locations^2", num_constraints = "num_facilities + num_locations + 3 * num_facilities * (num_facilities - 1) * num_locations^2", num_nonzeros = "2 * num_facilities * num_locations + 7 * num_facilities * (num_facilities - 1) * num_locations^2", - } -)] + }, + upper_bound { + max_constraint_magnitude_bits = "2", + }, +})] impl ReduceTo> for QuadraticAssignment { type Result = ReductionQAPToILP; diff --git a/src/rules/qubo_ilp.rs b/src/rules/qubo_ilp.rs index 9ed5218dc..fd67288ee 100644 --- a/src/rules/qubo_ilp.rs +++ b/src/rules/qubo_ilp.rs @@ -95,6 +95,7 @@ macro_rules! impl_qubo_to_ilp { ($coefficient:ty) => { #[reduction( transform = exact { + max_constraint_magnitude_bits = "1", num_vars = "num_vars + num_quadratic_terms", num_constraints = "3 * num_quadratic_terms", num_nonzeros = "7 * num_quadratic_terms", diff --git a/src/rules/rectilinearpicturecompression_ilp.rs b/src/rules/rectilinearpicturecompression_ilp.rs index 751aee09e..632fea4a8 100644 --- a/src/rules/rectilinearpicturecompression_ilp.rs +++ b/src/rules/rectilinearpicturecompression_ilp.rs @@ -39,11 +39,14 @@ impl ReductionResult for ReductionRPCToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionRPCToILP {} -#[reduction(transform = upper_bound { - num_vars = "num_rows^2 * num_cols^2", - num_constraints = "num_rows * num_cols + 1", - num_nonzeros = "(num_rows^2 * num_cols^2) * (num_rows * num_cols + 1)", -})] +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "num_rows^2 * num_cols^2 + 1", + num_vars = "num_rows^2 * num_cols^2", + num_constraints = "num_rows * num_cols + 1", + num_nonzeros = "(num_rows^2 * num_cols^2) * (num_rows * num_cols + 1)", + }, +)] impl ReduceTo> for RectilinearPictureCompression { type Result = ReductionRPCToILP; @@ -69,7 +72,11 @@ impl ReduceTo> for RectilinearPictureCompression { // Bound constraint: Σ x_r ≤ bound let bound_terms: Vec<(usize, i64)> = (0..num_vars).map(|i| (i, 1)).collect(); - constraints.push(LinearConstraint::le(bound_terms, self.bound())); + let max_rectangles = Self::exact_i64(num_vars, "encoding the rectangle-count bound")?; + constraints.push(LinearConstraint::le( + bound_terms, + self.bound().clamp(-1, max_rectangles), + )); let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; diff --git a/src/rules/registersufficiency_ilp.rs b/src/rules/registersufficiency_ilp.rs index b88b6d503..c126fe801 100644 --- a/src/rules/registersufficiency_ilp.rs +++ b/src/rules/registersufficiency_ilp.rs @@ -44,13 +44,16 @@ impl ReductionResult for ReductionRegisterSufficiencyToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionRegisterSufficiencyToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "3 * num_vertices^2 + num_vertices * (num_vertices - 1) / 2 + 2 * num_vertices", num_constraints = "9 * num_vertices^2 + 3 * num_vertices * (num_vertices - 1) / 2 + 3 * num_vertices + 2 * num_arcs + num_sinks", num_nonzeros = "18 * num_vertices^2 + 2 * num_vertices + 7 * num_vertices * (num_vertices - 1) / 2 + 4 * num_arcs + num_sinks", }, -)] + upper_bound { + max_constraint_magnitude_bits = "2 * num_vertices + bound + 1", + }, +})] impl ReduceTo> for RegisterSufficiency { type Result = ReductionRegisterSufficiencyToILP; diff --git a/src/rules/resourceconstrainedscheduling_ilp.rs b/src/rules/resourceconstrainedscheduling_ilp.rs index 37ef1f54f..640a5af18 100644 --- a/src/rules/resourceconstrainedscheduling_ilp.rs +++ b/src/rules/resourceconstrainedscheduling_ilp.rs @@ -53,6 +53,9 @@ impl ReductionResult for ReductionRCSToILP { impl crate::rules::AggregateReductionResult for ReductionRCSToILP {} #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "processor capacity and resource requirements and bounds are not registered source parameters", + }, exact { num_vars = "num_tasks * deadline", num_constraints = "num_tasks + deadline + num_resources * deadline", diff --git a/src/rules/rootedtreestorageassignment_ilp.rs b/src/rules/rootedtreestorageassignment_ilp.rs index b673dbeb9..27386196a 100644 --- a/src/rules/rootedtreestorageassignment_ilp.rs +++ b/src/rules/rootedtreestorageassignment_ilp.rs @@ -90,24 +90,32 @@ impl ReductionResult for ReductionRTSAToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionRTSAToILP {} -#[reduction(transform = upper_bound { - num_vars = "universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)", - num_constraints = "4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8)", - num_nonzeros = "(universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)) * (4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8))", -})] +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "universe_size * (num_subsets + 1) + 2", + num_vars = "universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)", + num_constraints = "4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8)", + num_nonzeros = "(universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)) * (4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8))", + }, +)] impl ReduceTo> for RootedTreeStorageAssignment { type Result = ReductionRTSAToILP; fn reduce_to(&self) -> Result { let n = self.universe_size(); let subsets = self.subsets(); - let bound = self.bound(); // Nontrivial subsets (size >= 2) let nontrivial: Vec = (0..subsets.len()) .filter(|&k| subsets[k].len() >= 2) .collect(); let r = nontrivial.len(); + // Each extension cost is between 0 and n-1; negative budgets are infeasible. + // Saturation is safe: the original i64 budget cannot exceed i64::MAX. + let max_cost = Self::exact_i64(r, "encoding the subset count")?.saturating_mul( + Self::exact_i64(n.saturating_sub(1), "encoding the maximum extension cost")?, + ); + let bound = self.bound().clamp(-1, max_cost); if n == 0 { return Ok(ReductionRTSAToILP { diff --git a/src/rules/ruralpostman_ilp.rs b/src/rules/ruralpostman_ilp.rs index 287153612..f70c6d0f9 100644 --- a/src/rules/ruralpostman_ilp.rs +++ b/src/rules/ruralpostman_ilp.rs @@ -46,6 +46,7 @@ impl ReductionResult for ReductionRPToILP { num_constraints = "2 * num_edges + num_required_edges + num_vertices + 2 * num_edges + num_vertices + 2 * num_edges + num_vertices + num_edges + num_edges + num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + num_edges + 2", num_nonzeros = "(num_edges + num_vertices + num_edges + num_vertices + 2 * num_edges) * (2 * num_edges + num_required_edges + num_vertices + 2 * num_edges + num_vertices + 2 * num_edges + num_vertices + num_edges + num_edges + num_vertices)", }, })] diff --git a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs index cb535f1ac..4b47cddfa 100644 --- a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs @@ -63,6 +63,9 @@ impl ReductionResult for ReductionSMWCTToILP { } #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "processing times are not bounded by registered source parameters", + }, exact { num_vars = "num_tasks * num_processors + num_tasks + num_tasks * (num_tasks - 1) / 2", num_constraints = "num_tasks + num_tasks * num_processors + 2 * num_tasks + 2 * num_tasks * (num_tasks - 1) / 2 * num_processors + num_tasks * (num_tasks - 1) / 2", diff --git a/src/rules/schedulingwithindividualdeadlines_ilp.rs b/src/rules/schedulingwithindividualdeadlines_ilp.rs index 034bc4c89..93df3a70c 100644 --- a/src/rules/schedulingwithindividualdeadlines_ilp.rs +++ b/src/rules/schedulingwithindividualdeadlines_ilp.rs @@ -63,6 +63,7 @@ impl crate::rules::AggregateReductionResult for ReductionSWIDToILP {} num_constraints = "num_tasks + max_deadline + num_precedences + 1", }, upper_bound { + max_constraint_magnitude_bits = "num_tasks + max_deadline + 1", num_nonzeros = "(num_tasks * max_deadline) * (num_tasks + max_deadline + num_precedences + 1)", }, })] @@ -80,8 +81,10 @@ impl ReduceTo> for SchedulingWithIndividualDeadlines { let num_vars = n * max_d; let var = |j: usize, t: usize| j * max_d + t; - let processor_count = - Self::exact_i64(self.num_processors(), "encoding the processor capacity")?; + let processor_count = Self::exact_i64( + self.num_processors().min(n), + "encoding the processor capacity", + )?; let mut constraints = Vec::new(); diff --git a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs index 1307eef41..f95236942 100644 --- a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs +++ b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs @@ -46,6 +46,9 @@ impl ReductionResult for ReductionSTMMCCToILP { } #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "task costs are not bounded by registered source parameters", + }, exact { num_vars = "num_tasks^2 + 1", num_constraints = "num_tasks^2 + 3 * num_tasks + num_precedences + 1", diff --git a/src/rules/sequencingtominimizetardytaskweight_ilp.rs b/src/rules/sequencingtominimizetardytaskweight_ilp.rs index b2f3c8a0a..0d13e5ea8 100644 --- a/src/rules/sequencingtominimizetardytaskweight_ilp.rs +++ b/src/rules/sequencingtominimizetardytaskweight_ilp.rs @@ -47,6 +47,9 @@ impl ReductionResult for ReductionSTMTTWToILP { } #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "task lengths and deadlines are not registered source parameters", + }, exact { num_vars = "num_tasks * num_tasks + num_tasks", num_constraints = "2 * num_tasks + 2 * num_tasks * num_tasks", diff --git a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index 46ab83739..a2e022d15 100644 --- a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -52,6 +52,9 @@ impl ReductionResult for ReductionSTMWCTToILP { } #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "processing times are not bounded by registered source parameters", + }, exact { num_vars = "num_tasks + num_tasks * (num_tasks - 1) / 2", num_constraints = "2 * num_tasks + 3 * num_tasks * (num_tasks - 1) / 2 + num_precedences", diff --git a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs index 928b711e5..96316c127 100644 --- a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -57,11 +57,16 @@ impl ReductionResult for ReductionSTMWTToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSTMWTToILP {} -#[reduction(transform = upper_bound { - num_vars = "num_tasks^2 + 2 * num_tasks", - num_constraints = "2 * num_tasks^2 + 3 * num_tasks + 1", - num_nonzeros = "(num_tasks^2 + 2 * num_tasks) * (2 * num_tasks^2 + 3 * num_tasks + 1)", -})] +#[reduction( + transform = upper_bound { + num_vars = "num_tasks^2 + 2 * num_tasks", + num_constraints = "2 * num_tasks^2 + 3 * num_tasks + 1", + num_nonzeros = "(num_tasks^2 + 2 * num_tasks) * (2 * num_tasks^2 + 3 * num_tasks + 1)", + }, + unavailable = { + max_constraint_magnitude_bits = "processing times, deadlines, weights and the cost bound are not registered source parameters", + }, +)] impl ReduceTo> for SequencingToMinimizeWeightedTardiness { type Result = ReductionSTMWTToILP; diff --git a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs index 2829801bf..58c35b4af 100644 --- a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs +++ b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs @@ -58,11 +58,16 @@ impl ReductionResult for ReductionSWDSTToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSWDSTToILP {} -#[reduction(transform = upper_bound { - num_vars = "2 * num_tasks^2 + num_tasks", - num_constraints = "2 * num_tasks + num_tasks^2 * (num_tasks - 1) + 3 * num_tasks * (num_tasks - 1) + num_tasks * num_tasks", - num_nonzeros = "(2 * num_tasks^2 + num_tasks) * (2 * num_tasks + num_tasks^2 * (num_tasks - 1) + 3 * num_tasks * (num_tasks - 1) + num_tasks * num_tasks)", -})] +#[reduction( + transform = upper_bound { + num_vars = "2 * num_tasks^2 + num_tasks", + num_constraints = "2 * num_tasks + num_tasks^2 * (num_tasks - 1) + 3 * num_tasks * (num_tasks - 1) + num_tasks * num_tasks", + num_nonzeros = "(2 * num_tasks^2 + num_tasks) * (2 * num_tasks + num_tasks^2 * (num_tasks - 1) + 3 * num_tasks * (num_tasks - 1) + num_tasks * num_tasks)", + }, + unavailable = { + max_constraint_magnitude_bits = "processing times, setup times and deadlines are not registered source parameters", + }, +)] impl ReduceTo> for SequencingWithDeadlinesAndSetUpTimes { type Result = ReductionSWDSTToILP; diff --git a/src/rules/sequencingwithinintervals_ilp.rs b/src/rules/sequencingwithinintervals_ilp.rs index 4182c20a7..1cb662236 100644 --- a/src/rules/sequencingwithinintervals_ilp.rs +++ b/src/rules/sequencingwithinintervals_ilp.rs @@ -77,6 +77,7 @@ impl ReductionResult for ReductionSWIToILP { impl crate::rules::AggregateReductionResult for ReductionSWIToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "num_start_slots", num_constraints = "num_start_slots^2 + num_tasks", num_nonzeros = "num_start_slots * (num_start_slots^2 + num_tasks)", diff --git a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs index 6d001eca3..48b960215 100644 --- a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs +++ b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs @@ -59,6 +59,7 @@ impl ReductionResult for ReductionSWRTDToILP { impl crate::rules::AggregateReductionResult for ReductionSWRTDToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "num_tasks * time_horizon", num_constraints = "num_tasks * time_horizon + num_tasks + time_horizon", num_nonzeros = "(num_tasks * time_horizon) * (num_tasks * time_horizon + num_tasks + time_horizon)", diff --git a/src/rules/setsplitting_ilp.rs b/src/rules/setsplitting_ilp.rs index ef0667f47..e7fc89213 100644 --- a/src/rules/setsplitting_ilp.rs +++ b/src/rules/setsplitting_ilp.rs @@ -52,6 +52,7 @@ impl crate::rules::AggregateReductionResult for ReductionSetSplittingToILP {} num_constraints = "2 * num_subsets", }, upper_bound { + max_constraint_magnitude_bits = "universe_size + 1", num_nonzeros = "universe_size * (2 * num_subsets)", }, })] @@ -63,6 +64,9 @@ impl ReduceTo> for SetSplitting { let mut constraints = Vec::new(); for subset in self.subsets() { + let mut subset = subset.clone(); + subset.sort_unstable(); + subset.dedup(); let terms: Vec<(usize, i64)> = subset.iter().map(|&e| (e, 1)).collect(); let k = >>::exact_i64( subset.len() - 1, @@ -72,7 +76,7 @@ impl ReduceTo> for SetSplitting { // At least one element in S2: sum >= 1 constraints.push(LinearConstraint::ge(terms.clone(), 1)); - // At least one element in S1: sum <= k - 1 + // At least one element in S1: sum <= |S| - 1 constraints.push(LinearConstraint::le(terms, k)); } diff --git a/src/rules/shortestcommonsupersequence_ilp.rs b/src/rules/shortestcommonsupersequence_ilp.rs index 784371a04..453e8e634 100644 --- a/src/rules/shortestcommonsupersequence_ilp.rs +++ b/src/rules/shortestcommonsupersequence_ilp.rs @@ -46,6 +46,7 @@ impl ReductionResult for ReductionSCSToILP { } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "max_length + 1", num_vars = "max_length * (alphabet_size + 1) + total_length * max_length", num_constraints = "max_length + total_length + total_length * max_length + total_length + max_length", num_nonzeros = "(max_length * (alphabet_size + 1) + total_length * max_length) * (max_length + total_length + total_length * max_length + total_length + max_length)", diff --git a/src/rules/shortestweightconstrainedpath_ilp.rs b/src/rules/shortestweightconstrainedpath_ilp.rs index 1811f8a95..423e205c2 100644 --- a/src/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/rules/shortestweightconstrainedpath_ilp.rs @@ -58,6 +58,9 @@ impl ReductionResult for ReductionSWCPToILP { } #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "edge weights and the weight bound are not registered source parameters", + }, exact { num_vars = "2 * num_edges + num_vertices", num_constraints = "5 * num_edges + 4 * num_vertices + 2", diff --git a/src/rules/sparsematrixcompression_ilp.rs b/src/rules/sparsematrixcompression_ilp.rs index 6b493c63b..bb5efda1d 100644 --- a/src/rules/sparsematrixcompression_ilp.rs +++ b/src/rules/sparsematrixcompression_ilp.rs @@ -46,6 +46,7 @@ impl ReductionResult for ReductionSMCToILP { impl crate::rules::AggregateReductionResult for ReductionSMCToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "num_rows * bound_k", num_constraints = "num_rows + num_rows^2 * num_cols^2 * bound_k", num_nonzeros = "(num_rows * bound_k) * (num_rows + num_rows^2 * num_cols^2 * bound_k)", diff --git a/src/rules/stackercrane_ilp.rs b/src/rules/stackercrane_ilp.rs index d4e15609a..1795e54ad 100644 --- a/src/rules/stackercrane_ilp.rs +++ b/src/rules/stackercrane_ilp.rs @@ -45,6 +45,7 @@ impl ReductionResult for ReductionSCToILP { } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "2", num_vars = "num_arcs * num_arcs + num_arcs * num_arcs * num_arcs", num_constraints = "num_arcs + num_arcs + 4 * num_arcs * num_arcs * num_arcs", num_nonzeros = "(num_arcs * num_arcs + num_arcs * num_arcs * num_arcs) * (num_arcs + num_arcs + 4 * num_arcs * num_arcs * num_arcs)", diff --git a/src/rules/steinertree_ilp.rs b/src/rules/steinertree_ilp.rs index 2dbc30918..e3e9a276c 100644 --- a/src/rules/steinertree_ilp.rs +++ b/src/rules/steinertree_ilp.rs @@ -49,6 +49,7 @@ impl ReductionResult for ReductionSteinerTreeToILP { num_constraints = "num_vertices * (num_vertices - 1) + 2 * num_edges * num_vertices + num_terminals + 1", }, upper_bound { + max_constraint_magnitude_bits = "1", num_nonzeros = "(num_edges + num_vertices + 2 * num_edges * (num_vertices - 1)) * (num_vertices * (num_vertices - 1) + 2 * num_edges * num_vertices + num_terminals + 1)", }, })] diff --git a/src/rules/stringtostringcorrection_ilp.rs b/src/rules/stringtostringcorrection_ilp.rs index 1f99ce66c..f8b674bcf 100644 --- a/src/rules/stringtostringcorrection_ilp.rs +++ b/src/rules/stringtostringcorrection_ilp.rs @@ -116,6 +116,7 @@ impl ReductionResult for ReductionSTSCToILP { impl crate::rules::AggregateReductionResult for ReductionSTSCToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "(bound + 1) * source_length^2 + (bound + 1) * source_length + 2 * bound * source_length + bound", num_constraints = "4 * bound * source_length^3 + 2 * bound * source_length^2 + source_length^2 + 6 * bound * source_length + 5 * source_length + bound + 1", num_nonzeros = "((bound + 1) * source_length^2 + (bound + 1) * source_length + 2 * bound * source_length + bound) * (4 * bound * source_length^3 + 2 * bound * source_length^2 + source_length^2 + 6 * bound * source_length + 5 * source_length + bound + 1)", diff --git a/src/rules/strongconnectivityaugmentation_ilp.rs b/src/rules/strongconnectivityaugmentation_ilp.rs index bdae17fb3..8a76cd23e 100644 --- a/src/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/rules/strongconnectivityaugmentation_ilp.rs @@ -44,11 +44,16 @@ impl ReductionResult for ReductionSCAToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSCAToILP {} -#[reduction(transform = upper_bound { - num_vars = "num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)", - num_constraints = "1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices", - num_nonzeros = "(num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)) * (1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices)", -})] +#[reduction( + transform = upper_bound { + num_vars = "num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)", + num_constraints = "1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices", + num_nonzeros = "(num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)) * (1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices)", + }, + unavailable = { + max_constraint_magnitude_bits = "candidate arc weights and the budget are not registered source parameters", + }, +)] impl ReduceTo> for StrongConnectivityAugmentation { type Result = ReductionSCAToILP; diff --git a/src/rules/subgraphisomorphism_ilp.rs b/src/rules/subgraphisomorphism_ilp.rs index 55a07a90b..83873c85c 100644 --- a/src/rules/subgraphisomorphism_ilp.rs +++ b/src/rules/subgraphisomorphism_ilp.rs @@ -58,6 +58,7 @@ impl ReductionResult for ReductionSubIsoToILP { impl crate::rules::AggregateReductionResult for ReductionSubIsoToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "2", num_vars = "num_pattern_vertices * num_host_vertices", num_constraints = "num_pattern_vertices + num_host_vertices + num_pattern_edges * num_host_vertices^2", num_nonzeros = "(num_pattern_vertices * num_host_vertices) * (num_pattern_vertices + num_host_vertices + num_pattern_edges * num_host_vertices^2)", diff --git a/src/rules/subsetsum_partition.rs b/src/rules/subsetsum_partition.rs index ae35fb792..d59308bc3 100644 --- a/src/rules/subsetsum_partition.rs +++ b/src/rules/subsetsum_partition.rs @@ -68,8 +68,10 @@ impl ReductionResult for ReductionSubsetSumToPartition { #[crate::aggregate_reduction(identity)] impl crate::rules::AggregateReductionResult for ReductionSubsetSumToPartition {} +// The padding |sum(sizes) - 2*target| is below (n+2)*2^h <= 2^(h+n+1). #[reduction( transform = upper_bound { + max_numeric_magnitude_bits = "max_numeric_magnitude_bits + num_elements + 1", num_elements = "num_elements + 1", })] impl ReduceTo for SubsetSum { diff --git a/src/rules/sumofsquarespartition_ilp.rs b/src/rules/sumofsquarespartition_ilp.rs index 57ad1b258..83d036587 100644 --- a/src/rules/sumofsquarespartition_ilp.rs +++ b/src/rules/sumofsquarespartition_ilp.rs @@ -72,13 +72,16 @@ impl ReductionResult for ReductionSSPToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_elements * num_groups + num_elements^2 * num_groups", num_constraints = "num_elements + 3 * num_elements^2 * num_groups", num_nonzeros = "7 * num_elements^2 * num_groups", }, -)] + upper_bound { + max_constraint_magnitude_bits = "2", + }, +})] impl ReduceTo> for SumOfSquaresPartition { type Result = ReductionSSPToILP; diff --git a/src/rules/threedimensionalmatching_ilp.rs b/src/rules/threedimensionalmatching_ilp.rs index a7a325b9e..14f62f061 100644 --- a/src/rules/threedimensionalmatching_ilp.rs +++ b/src/rules/threedimensionalmatching_ilp.rs @@ -38,6 +38,7 @@ impl crate::rules::AggregateReductionResult for ReductionThreeDimensionalMatchin #[reduction( transform = exact { + max_constraint_magnitude_bits = "1", num_vars = "num_triples", num_constraints = "3 * universe_size", num_nonzeros = "3 * num_triples", diff --git a/src/rules/timetabledesign_ilp.rs b/src/rules/timetabledesign_ilp.rs index 0964aeae9..07a612bea 100644 --- a/src/rules/timetabledesign_ilp.rs +++ b/src/rules/timetabledesign_ilp.rs @@ -64,11 +64,14 @@ impl ReductionResult for ReductionTDToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionTDToILP {} -#[reduction(transform = upper_bound { - num_vars = "num_craftsmen * num_tasks * num_periods", - num_constraints = "num_craftsmen * num_periods + num_tasks * num_periods + num_craftsmen * num_tasks + num_craftsmen * num_tasks * num_periods", - num_nonzeros = "(num_craftsmen * num_tasks * num_periods) * (num_craftsmen * num_periods + num_tasks * num_periods + num_craftsmen * num_tasks + num_craftsmen * num_tasks * num_periods)", -})] +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "num_periods + 2", + num_vars = "num_craftsmen * num_tasks * num_periods", + num_constraints = "num_craftsmen * num_periods + num_tasks * num_periods + num_craftsmen * num_tasks + num_craftsmen * num_tasks * num_periods", + num_nonzeros = "(num_craftsmen * num_tasks * num_periods) * (num_craftsmen * num_periods + num_tasks * num_periods + num_craftsmen * num_tasks + num_craftsmen * num_tasks * num_periods)", + }, +)] impl ReduceTo> for TimetableDesign { type Result = ReductionTDToILP; @@ -77,6 +80,8 @@ impl ReduceTo> for TimetableDesign { let nt = self.num_tasks(); let nh = self.num_periods(); let requirements = self.requirements(); + // A pair can work at most nh periods. Keep out-of-range requirements infeasible. + let max_requirement = Self::exact_i64(nh, "encoding the period count")?.saturating_add(1); let num_vars = nc * nt * nh; let var = |c: usize, t: usize, h: usize| -> usize { ((c * nt) + t) * nh + h }; @@ -114,7 +119,10 @@ impl ReduceTo> for TimetableDesign { for (c, row) in requirements.iter().enumerate() { for (t, &requirement) in row.iter().enumerate() { let terms: Vec<(usize, i64)> = (0..nh).map(|h| (var(c, t, h), 1)).collect(); - constraints.push(LinearConstraint::eq(terms, requirement)); + constraints.push(LinearConstraint::eq( + terms, + requirement.clamp(-1, max_requirement), + )); } } diff --git a/src/rules/travelingsalesman_ilp.rs b/src/rules/travelingsalesman_ilp.rs index 7f219ae13..c2fa7d68d 100644 --- a/src/rules/travelingsalesman_ilp.rs +++ b/src/rules/travelingsalesman_ilp.rs @@ -71,6 +71,7 @@ impl ReductionResult for ReductionTSPToILP { num_constraints = "num_vertices^3 + -1 * num_vertices^2 + 2 * num_vertices + 4 * num_vertices * num_edges", }, upper_bound { + max_constraint_magnitude_bits = "2", num_nonzeros = "(num_vertices^2 + 2 * num_vertices * num_edges) * (num_vertices^3 + -1 * num_vertices^2 + 2 * num_vertices + 4 * num_vertices * num_edges)", }, })] diff --git a/src/rules/undirectedflowlowerbounds_ilp.rs b/src/rules/undirectedflowlowerbounds_ilp.rs index 35cf0642b..acb9f5442 100644 --- a/src/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/rules/undirectedflowlowerbounds_ilp.rs @@ -78,11 +78,16 @@ impl ReductionResult for ReductionUFLBToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionUFLBToILP {} -#[reduction(transform = upper_bound { - num_vars = "3 * num_edges", - num_constraints = "5 * num_edges + num_vertices + 1", - num_nonzeros = "(3 * num_edges) * (5 * num_edges + num_vertices + 1)", -})] +#[reduction( + transform = upper_bound { + num_vars = "3 * num_edges", + num_constraints = "5 * num_edges + num_vertices + 1", + num_nonzeros = "(3 * num_edges) * (5 * num_edges + num_vertices + 1)", + }, + unavailable = { + max_constraint_magnitude_bits = "flow capacities, lower bounds and the requirement are not registered source parameters", + }, +)] impl ReduceTo> for UndirectedFlowLowerBounds { type Result = ReductionUFLBToILP; diff --git a/src/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/rules/undirectedtwocommodityintegralflow_ilp.rs index 07da247ea..52e9affbd 100644 --- a/src/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -70,6 +70,9 @@ impl ReductionResult for ReductionU2CIFToILP { impl crate::rules::AggregateReductionResult for ReductionU2CIFToILP {} #[reduction(transform = { + unavailable { + max_constraint_magnitude_bits = "flow capacities and requirements are not registered source parameters", + }, exact { num_vars = "6 * num_edges", num_constraints = "7 * num_edges + num_conservation_constraints + 2", diff --git a/src/types.rs b/src/types.rs index 03146feba..10d27bdcb 100644 --- a/src/types.rs +++ b/src/types.rs @@ -55,6 +55,41 @@ pub trait NumericSize: fn checked_mul_value(self, other: Self) -> Result; } +/// Smallest h >= 1 for which every finite input has magnitude below 2^h. +/// Halving preserves integer and floating power-of-two boundaries without +/// taking an absolute value (which would overflow for i64::MIN). +pub(crate) fn max_numeric_magnitude_bits( + values: impl IntoIterator, +) -> u64 { + fn bits(mut value: C) -> u64 { + let one = C::one(); + let two = one.clone() + one.clone(); + let negative = value < C::zero(); + let unit = if negative { C::zero() - one } else { one }; + let mut bits = 0; + while if negative { + value <= unit + } else { + value >= unit + } { + value = value / two.clone(); + bits += 1; + } + bits.max(1) + } + + // Only the extrema need bit counting; scanning all entries is linear. + let (mut minimum, mut maximum) = (C::zero(), C::zero()); + for value in values { + if value < minimum { + minimum = value; + } else if value > maximum { + maximum = value; + } + } + bits(minimum).max(bits(maximum)) +} + macro_rules! impl_integer_numeric_size { ($($type:ty),* $(,)?) => { $( diff --git a/src/unit_tests/models/algebraic/ilp.rs b/src/unit_tests/models/algebraic/ilp.rs index a7539acec..886896488 100644 --- a/src/unit_tests/models/algebraic/ilp.rs +++ b/src/unit_tests/models/algebraic/ilp.rs @@ -3,6 +3,104 @@ use crate::solvers::{ILPSolveError, ILPSolver}; use crate::traits::Problem; use crate::types::Extremum; +#[test] +fn constraint_magnitude_bits_measure_normalized_integer_data() { + for (value, expected) in [ + (0, 1), + (1, 1), + (-1, 1), + (2, 2), + (-2, 2), + (7, 3), + (8, 4), + ((1_i64 << 54) - 1, 54), + (i64::MAX, 63), + (i64::MIN, 64), + ] { + for row in [ + LinearConstraint::le(vec![(0, value)], 0), + LinearConstraint::ge(vec![], value), + ] { + let source = binary_ilp(1, vec![row], vec![(0, i64::MAX)], ObjectiveSense::Minimize); + assert_eq!( + source.parameters().get("max_constraint_magnitude_bits"), + Some(expected), + "{value}" + ); + } + } + let source = binary_ilp( + 1, + vec![LinearConstraint::le( + vec![(0, 100), (0, -100), (0, 3), (0, 5)], + 0, + )], + vec![], + ObjectiveSense::Minimize, + ); + assert_eq!( + source.parameters().get("max_constraint_magnitude_bits"), + Some(4) + ); +} + +#[test] +fn constraint_magnitude_bits_include_finite_endpoints_without_requiring_boundedness() { + for (lower, upper, expected) in [ + (None, None, 1), + (Some(-8), None, 4), + (None, Some(16), 5), + (Some(i64::MIN), Some(i64::MAX), 64), + ] { + let source = ILP::::with_variables( + vec![IntegerVariable::new(lower, upper).unwrap()], + vec![], + vec![(0, i64::MAX)], + ObjectiveSense::Minimize, + ) + .unwrap(); + assert_eq!( + source.parameters().get("max_constraint_magnitude_bits"), + Some(expected) + ); + } + assert_eq!( + ILP::::empty() + .parameters() + .get("max_constraint_magnitude_bits"), + Some(1) + ); +} + +#[test] +fn constraint_magnitude_bits_cover_float_exponents_without_rounding_boundaries() { + for (value, expected) in [ + (0.0, 1), + (-0.0, 1), + (f64::from_bits(1), 1), + (0.5, 1), + (f64::from_bits(8.0_f64.to_bits() - 1), 3), + (8.0, 4), + (-8.0, 4), + (f64::MAX, 1024), + (-f64::MAX, 1024), + ] { + for row in [ + LinearConstraint::le(vec![(0, value)], 0.0), + LinearConstraint::ge(vec![], value), + ] { + let source = + ILP::::new(1, vec![row], vec![(0, f64::MAX)], ObjectiveSense::Minimize) + .unwrap(); + assert_eq!( + source.parameters().get("max_constraint_magnitude_bits"), + Some(expected), + "{value}" + ); + } + } +} + #[test] fn bounded_integer_ilp_loading_requires_finite_domains() { assert!(ILP::::new(1, vec![], vec![], ObjectiveSense::Minimize).is_ok()); diff --git a/src/unit_tests/models/misc/bin_packing.rs b/src/unit_tests/models/misc/bin_packing.rs index b0a8e77eb..9564a2dc0 100644 --- a/src/unit_tests/models/misc/bin_packing.rs +++ b/src/unit_tests/models/misc/bin_packing.rs @@ -154,3 +154,37 @@ fn test_bin_packing_rejects_non_finite_values() { assert!(BinPacking::new(vec![f64::NAN], 1.0).is_err()); assert!(BinPacking::new(vec![1.0], f64::INFINITY).is_err()); } + +#[test] +fn numeric_magnitude_bits_cover_sizes_capacity_and_float_boundaries() { + for (sizes, capacity, expected) in [ + (vec![], 0_i64, 1), + (vec![1], 8, 4), + (vec![8], 1, 4), + (vec![(1_i64 << 54) - 1], 1, 54), + (vec![i64::MIN], 1, 64), + (vec![1], i64::MAX, 63), + ] { + let source = BinPacking::new(sizes, capacity).unwrap(); + assert_eq!( + source.parameters().get("max_numeric_magnitude_bits"), + Some(expected) + ); + } + for (sizes, capacity, expected) in [ + (vec![f64::from_bits(1)], 0.5, 1), + (vec![], f64::from_bits(8.0_f64.to_bits() - 1), 3), + (vec![-8.0], 1.0, 4), + (vec![1.0], f64::MAX, 1024), + ] { + let source = BinPacking::new(sizes, capacity).unwrap(); + assert_eq!( + source.parameters().get("max_numeric_magnitude_bits"), + Some(expected) + ); + } + assert_eq!( + BinPacking::::parameter_names(), + BinPacking::::parameter_names() + ); +} diff --git a/src/unit_tests/models/misc/partition.rs b/src/unit_tests/models/misc/partition.rs index f6a969235..9177e1341 100644 --- a/src/unit_tests/models/misc/partition.rs +++ b/src/unit_tests/models/misc/partition.rs @@ -121,3 +121,20 @@ fn test_partition_rejects_zero_size() { fn test_partition_rejects_empty_input() { assert!(Partition::new(vec![]).is_err()); } + +#[test] +fn numeric_magnitude_bits_measure_elements_without_summing_them() { + for (sizes, expected) in [ + (vec![1, 1], 1), + (vec![7, 1], 3), + (vec![8, 1], 4), + (vec![(1_i64 << 54) - 1], 54), + (vec![i64::MAX], 63), + ] { + let source = Partition::new(sizes).unwrap(); + assert_eq!( + source.parameters().get("max_numeric_magnitude_bits"), + Some(expected) + ); + } +} diff --git a/src/unit_tests/models/misc/subset_sum.rs b/src/unit_tests/models/misc/subset_sum.rs index b9686cc93..fffee4f93 100644 --- a/src/unit_tests/models/misc/subset_sum.rs +++ b/src/unit_tests/models/misc/subset_sum.rs @@ -202,3 +202,22 @@ fn test_subsetsum_large_integer_input() { .evaluate(&vec![true, true, false, false, false, false]) .unwrap()); // 3 + 7 = 10 } + +#[test] +fn numeric_magnitude_bits_include_arbitrary_precision_sizes_and_target() { + let huge = BigUint::from(1_u8) << 1000_usize; + for (sizes, target, expected) in [ + (vec![], BigUint::from(0_u8), 1), + (vec![BigUint::from(7_u8)], BigUint::from(0_u8), 3), + (vec![BigUint::from(1_u8)], BigUint::from(8_u8), 4), + (vec![&huge - 1_u8], BigUint::from(0_u8), 1000), + (vec![huge.clone()], BigUint::from(0_u8), 1001), + (vec![BigUint::from(1_u8)], huge, 1001), + ] { + let source = SubsetSum::new(sizes, target); + assert_eq!( + source.parameters().get("max_numeric_magnitude_bits"), + Some(expected) + ); + } +} diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index d059532f8..555385530 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -11,6 +11,146 @@ use crate::types::ProblemParameters; use crate::variant::{K3, KN}; use std::collections::BTreeMap; +#[test] +fn domination_magnitude_predictions_account_for_parallel_edges() { + let source = MinimumDominatingSet::new(SimpleGraph::new(2, vec![(0, 1); 8]), vec![1_i64; 2]); + let target = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!( + target + .target_problem() + .parameters() + .get("max_constraint_magnitude_bits"), + Some(4) + ); + let entries = crate::rules::registry::reduction_entries(); + let entry = entries + .iter() + .find(|entry| entry.source_name == "MinimumDominatingSet" && entry.target_name == "ILP") + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let predicted = contract + .transform() + .unwrap() + .evaluate(&source.parameters()) + .unwrap(); + assert!(predicted.get("max_constraint_magnitude_bits").unwrap() >= 4); +} + +#[test] +fn decision_ilp_contracts_distinguish_bounded_and_unrepresented_numeric_data() { + for (name, magnitude_available) in [ + ("DecisionLongestCircuit", false), + ("DecisionOpenShopScheduling", true), + ] { + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| entry.source_name == name && entry.target_name == "ILP") + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let transform = contract.transform().unwrap(); + assert_eq!( + contract + .unavailable() + .iter() + .any(|field| field.field == "max_constraint_magnitude_bits"), + !magnitude_available, + "{name}" + ); + assert_eq!( + transform.get("max_constraint_magnitude_bits").is_some(), + magnitude_available + ); + for field in ["num_vars", "num_constraints", "num_nonzeros"] { + assert!(transform.get(field).is_some(), "{name}: {field}"); + } + } +} + +#[test] +fn bounded_ilp_size_predictions_compose_through_translated_domains() { + use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense}; + let path = ReductionPath { + steps: vec![ + ReductionStep { + name: "ILP".into(), + variant: ReductionGraph::variant_to_map(&ILP::::variant()), + }, + ReductionStep { + name: "ILP".into(), + variant: ReductionGraph::variant_to_map(&ILP::::variant()), + }, + ReductionStep { + name: "QUBO".into(), + variant: ReductionGraph::variant_to_map(&QUBO::::variant()), + }, + ], + }; + let graph = ReductionGraph::new(); + let transform = graph + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap(); + for lower in [-1, -8] { + let source = ILP::::with_variables( + vec![IntegerVariable::new(Some(lower), Some(lower + 1)).unwrap()], + vec![LinearConstraint::le(vec![(0, 1)], 0)], + vec![(0, 1)], + ObjectiveSense::Minimize, + ) + .unwrap(); + let binary = ReduceTo::>::reduce_to(&source).unwrap(); + let qubo = ReduceTo::>::reduce_to(binary.target_problem()).unwrap(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + for (field, actual) in qubo.target_problem().parameters().iter() { + assert!(predicted.get(field).expect("composed size bound") >= actual); + } + let solution = BruteForce::new() + .solve(qubo.target_problem()) + .unwrap() + .unwrap(); + let recovered = binary + .extract_solution(&qubo.extract_solution(&solution).unwrap()) + .unwrap(); + assert_eq!(recovered, vec![lower]); + } +} + +#[test] +fn weighted_qubo_round_trip_size_prediction_uses_only_structural_parameters() { + let path = ReductionPath { + steps: vec![ + ReductionStep { + name: "QUBO".into(), + variant: ReductionGraph::variant_to_map(&QUBO::::variant()), + }, + ReductionStep { + name: "ILP".into(), + variant: ReductionGraph::variant_to_map(&ILP::::variant()), + }, + ReductionStep { + name: "QUBO".into(), + variant: ReductionGraph::variant_to_map(&QUBO::::variant()), + }, + ], + }; + let transform = ReductionGraph::new() + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap(); + for weight in [1, 1000] { + let source = QUBO::from_matrix(vec![vec![-weight, 1], vec![0, -weight]]).unwrap(); + let ilp = ReduceTo::>::reduce_to(&source).unwrap(); + let qubo = ReduceTo::>::reduce_to(ilp.target_problem()).unwrap(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + // Three binary ILP variables and three unit-magnitude rows give U=15. + assert_eq!(predicted.get("num_vars"), Some(15)); + assert_eq!(predicted.get("num_quadratic_terms"), Some(225)); + for (field, actual) in qubo.target_problem().parameters().iter() { + assert!(predicted.get(field).unwrap() >= actual); + } + } +} + #[test] fn integer_ilp_graph_requires_bounded_domains_for_binary_encoding() { let graph = ReductionGraph::new(); @@ -1160,3 +1300,127 @@ fn test_find_paths_bounded_returns_shortest_when_truncated() { ); assert_eq!(lens, vec![1, 4]); } + +#[test] +fn knapsack_normalization_restores_composed_qubo_predictions() { + use crate::models::misc::Knapsack; + let path = ReductionPath { + steps: vec![ + ReductionStep { + name: Knapsack::NAME.into(), + variant: ReductionGraph::variant_to_map(&Knapsack::variant()), + }, + ReductionStep { + name: ILP::::NAME.into(), + variant: ReductionGraph::variant_to_map(&ILP::::variant()), + }, + ReductionStep { + name: QUBO::::NAME.into(), + variant: ReductionGraph::variant_to_map(&QUBO::::variant()), + }, + ], + }; + let transform = ReductionGraph::new() + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap(); + for capacity in [0, 1] { + let source = Knapsack::new(vec![0, 1, i64::MAX], vec![2, 3, 4], capacity); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + let ilp = ReduceTo::>::reduce_to(&source).unwrap(); + let qubo = ReduceTo::>::reduce_to(ilp.target_problem()).unwrap(); + for (field, actual) in qubo.target_problem().parameters().iter() { + assert!(predicted.get(field).unwrap() >= actual); + } + } +} + +#[test] +fn numeric_magnitude_bits_propagate_from_sat_to_qubo() { + use crate::models::formula::CNFClause; + use crate::models::misc::{BinPacking, Partition, SubsetSum}; + let path = ReductionPath { + steps: [ + ( + KSatisfiability::::NAME, + KSatisfiability::::variant(), + ), + (SubsetSum::NAME, SubsetSum::variant()), + (Partition::NAME, Partition::variant()), + (BinPacking::::NAME, BinPacking::::variant()), + (ILP::::NAME, ILP::::variant()), + (QUBO::::NAME, QUBO::::variant()), + ] + .into_iter() + .map(|(name, variant)| ReductionStep { + name: name.into(), + variant: ReductionGraph::variant_to_map(&variant), + }) + .collect(), + }; + let graph = ReductionGraph::new(); + let transform = graph + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap(); + for source in [ + KSatisfiability::::new(1, vec![]), + KSatisfiability::::new(3, vec![CNFClause::new(vec![1, 2, 3])]), + ] { + let predicted = transform.evaluate(&source.parameters()).unwrap(); + let chain = graph.reduce_along_path(&path, &source).unwrap().unwrap(); + let target = chain.target_problem::>(); + for (field, actual) in target.parameters().iter() { + assert!(predicted.get(field).unwrap() >= actual); + } + if source.num_vars() == 1 { + let solution = BruteForce::new().solve(target).unwrap().unwrap(); + let recovered: Vec = chain.extract_solution(&solution).unwrap(); + assert!(source.evaluate(&recovered).unwrap().0); + } + } +} + +#[test] +fn numeric_magnitude_bits_cover_padding_and_empty_targets() { + use crate::models::misc::{BinPacking, Partition, SubsetSum}; + fn check(source: S) + where + S: Problem + ReduceTo, + T: Problem, + { + let reduction = source.reduce_to().unwrap(); + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| { + entry.source_name == S::NAME + && entry.source_variant() == S::variant() + && entry.target_name == T::NAME + && entry.target_variant() == T::variant() + }) + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let predicted = contract + .transform() + .unwrap() + .evaluate(&source.parameters()) + .unwrap(); + for (field, actual) in reduction.target_problem().parameters().iter() { + assert!( + predicted.get(field).unwrap() >= actual, + "{} -> {}: {field}", + S::NAME, + T::NAME + ); + } + } + for target in [0_u32, 1, 2, 1000] { + check::<_, Partition>(SubsetSum::new(vec![1_u32, 1], target)); + } + for sizes in [vec![1], vec![1, 1], vec![7, 1], vec![7, 7, 7, 7]] { + check::<_, SubsetSum>(Partition::new(sizes.clone()).unwrap()); + check::<_, BinPacking>(Partition::new(sizes).unwrap()); + } + check::<_, ILP>(BinPacking::new(Vec::::new(), i64::MAX).unwrap()); + check::<_, ILP>(BinPacking::new(vec![8_i64], 1).unwrap()); +} diff --git a/src/unit_tests/rules/consecutiveblockminimization_ilp.rs b/src/unit_tests/rules/consecutiveblockminimization_ilp.rs index f778a2f96..fe9267c0a 100644 --- a/src/unit_tests/rules/consecutiveblockminimization_ilp.rs +++ b/src/unit_tests/rules/consecutiveblockminimization_ilp.rs @@ -62,3 +62,29 @@ fn test_cbm_to_ilp_trivial() { // x: 1, a: 1, b: 1 => 3 assert_eq!(ilp.num_vars(), 3); } + +#[test] +fn test_block_bound_normalization_preserves_feasibility() { + for matrix in [vec![], vec![vec![]], vec![vec![true]]] { + for bound in [i64::MIN, -1, 0, 1, i64::MAX] { + let source = ConsecutiveBlockMinimization::new(matrix.clone(), bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!( + reduction.target_problem().max_constraint_magnitude_bits(), + 1 + ); + let solution = ILPSolver::new().solve(reduction.target_problem()); + let minimum_blocks = i64::from(source.num_cols() != 0); + assert_eq!(solution.is_ok(), bound >= minimum_blocks); + if let Ok(solution) = solution { + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!(source.evaluate(&recovered).unwrap(), Or(true)); + } else { + assert_eq!( + solution.unwrap_err(), + crate::solvers::ILPSolveError::Infeasible + ); + } + } + } +} diff --git a/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs b/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs index 1a9169afe..6c53a903d 100644 --- a/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs +++ b/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs @@ -73,3 +73,22 @@ fn test_coma_to_ilp_trivial() { // x: 1, a+l+u+h+f: 5*1=5 => 6 assert_eq!(ilp.num_vars(), 6); } + +#[test] +fn test_augmentation_threshold_normalization() { + for bound in [0, 1, i64::MAX] { + let source = ConsecutiveOnesMatrixAugmentation::new(vec![vec![true]], bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!( + reduction.target_problem().max_constraint_magnitude_bits(), + 1 + ); + let solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Or(true) + ); + } +} diff --git a/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs b/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs index 644db2151..63b9b4e3c 100644 --- a/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs @@ -188,3 +188,32 @@ fn test_directedtwocommodityintegralflow_to_ilp_preserves_large_exact_capacity() assert_eq!(capacity_constraint.terms(), vec![(0, 1), (1, 1)]); assert_eq!(capacity_constraint.rhs(), capacity); } + +#[test] +fn test_flow_requirement_normalization_preserves_feasibility() { + for requirement in [0, 1, 2, i64::MAX] { + let source = DirectedTwoCommodityIntegralFlow::new( + DirectedGraph::new(4, vec![(0, 1), (2, 3)]), + vec![1, 1], + 0, + 1, + 2, + 3, + requirement, + requirement, + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(requirement <= 1); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert!(source.evaluate(&recovered).unwrap().0); + } + Err(error) => { + assert!(requirement > 1); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/rules/ilp_qubo.rs b/src/unit_tests/rules/ilp_qubo.rs index 3f89c648d..3ba2dada1 100644 --- a/src/unit_tests/rules/ilp_qubo.rs +++ b/src/unit_tests/rules/ilp_qubo.rs @@ -57,6 +57,20 @@ fn parameter_bounds_cover_slack_boundaries_and_cancellation() { ); } } + + // A unit row needs a small, data-dependent bound even when objective weights grow. + for weight in [0, 1000] { + let source = ILP::::new( + 1, + vec![LinearConstraint::le(vec![(0, 1)], 1)], + vec![(0, weight)], + ObjectiveSense::Minimize, + ) + .unwrap(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + assert_eq!(predicted.get("num_vars"), Some(3)); + assert_eq!(predicted.get("num_quadratic_terms"), Some(9)); + } } #[test] diff --git a/src/unit_tests/rules/integerknapsack_ilp.rs b/src/unit_tests/rules/integerknapsack_ilp.rs index e8fea50f7..2c51e527f 100644 --- a/src/unit_tests/rules/integerknapsack_ilp.rs +++ b/src/unit_tests/rules/integerknapsack_ilp.rs @@ -107,3 +107,29 @@ fn test_integerknapsack_to_ilp_canonical_example_spec() { serde_json::json!([0, 0, 2]) ); } + +#[test] +fn test_integer_knapsack_normalization_excludes_oversized_items_through_qubo() { + use crate::models::algebraic::QUBO; + use crate::traits::Problem; + for capacity in [0, 2] { + let source = IntegerKnapsack::new(vec![1, i64::MAX], vec![3, 4], capacity).unwrap(); + let ilp = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(ilp.target_problem().max_constraint_magnitude_bits() <= 2); + let binary = ReduceTo::>::reduce_to(ilp.target_problem()).unwrap(); + let qubo = ReduceTo::>::reduce_to(binary.target_problem()).unwrap(); + let solution = crate::solvers::BruteForce::new() + .solve(qubo.target_problem()) + .unwrap() + .unwrap(); + let recovered = ilp + .extract_solution( + &binary + .extract_solution(&qubo.extract_solution(&solution).unwrap()) + .unwrap(), + ) + .unwrap(); + assert_eq!(recovered, vec![usize::try_from(capacity).unwrap(), 0]); + assert_eq!(source.evaluate(&recovered).unwrap().0, Some(3 * capacity)); + } +} diff --git a/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs b/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs index 51c5352ab..60bc0a9b7 100644 --- a/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs +++ b/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs @@ -48,3 +48,30 @@ fn test_integralflowhomologousarcs_to_ilp_bf_vs_ilp() { ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } + +#[test] +fn test_flow_requirement_normalization_preserves_feasibility() { + for requirement in [i64::MIN, 0, 1, 2, i64::MAX] { + let source = IntegralFlowHomologousArcs::new( + DirectedGraph::new(2, vec![(0, 1)]), + vec![1], + 0, + 1, + requirement, + vec![], + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(requirement <= 1); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert!(source.evaluate(&recovered).unwrap()); + } + Err(error) => { + assert!(requirement > 1); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/rules/knapsack_ilp.rs b/src/unit_tests/rules/knapsack_ilp.rs index bf72d4cff..36c11a5c4 100644 --- a/src/unit_tests/rules/knapsack_ilp.rs +++ b/src/unit_tests/rules/knapsack_ilp.rs @@ -130,3 +130,26 @@ fn test_knapsack_to_ilp_canonical_example_spec() { }] ); } + +#[test] +fn test_knapsack_normalization_excludes_oversized_items_through_qubo() { + use crate::models::algebraic::QUBO; + for capacity in [0, 1] { + let source = Knapsack::new(vec![0, 1, i64::MAX], vec![2, 3, 4], capacity); + let ilp = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!(ilp.target_problem().max_constraint_magnitude_bits(), 1); + let qubo = ReduceTo::>::reduce_to(ilp.target_problem()).unwrap(); + let solution = BruteForce::new() + .solve(qubo.target_problem()) + .unwrap() + .unwrap(); + let recovered = ilp + .extract_solution(&qubo.extract_solution(&solution).unwrap()) + .unwrap(); + assert_eq!(recovered, vec![true, capacity == 1, false]); + assert_eq!( + source.evaluate(&recovered).unwrap().0, + Some(2 + 3 * capacity) + ); + } +} diff --git a/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs b/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs index c9ca738b9..595c3bc38 100644 --- a/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs +++ b/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs @@ -116,3 +116,19 @@ fn test_lengthboundeddisjointpaths_to_ilp_rejects_invalid_target_solutions() { assert!(reduction.extract_solution(&solution).is_err()); } } + +#[test] +fn test_path_length_threshold_normalization() { + for bound in [1, 2, 1000] { + let source = + LengthBoundedDisjointPaths::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 0, 2, bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + let solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!( + source.evaluate(&recovered).unwrap(), + Max(Some(i64::from(bound >= 2))) + ); + } +} diff --git a/src/unit_tests/rules/maximumcokplex_ilp.rs b/src/unit_tests/rules/maximumcokplex_ilp.rs index aa92d32f2..8e479e61f 100644 --- a/src/unit_tests/rules/maximumcokplex_ilp.rs +++ b/src/unit_tests/rules/maximumcokplex_ilp.rs @@ -88,3 +88,19 @@ fn test_maximumcokplex_to_ilp_extract_solution_identity() { assert_eq!(extracted, vec![true, false, true, false, true]); assert_eq!(source.evaluate(&extracted).unwrap(), Max(Some(12))); } + +#[test] +fn test_cokplex_threshold_normalization() { + for k in [1, 2, 1000] { + let source = + MaximumCoKPlex::<_, i64, KN>::with_k(SimpleGraph::new(2, vec![(0, 1)]), vec![2, 3], k); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + let solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!( + source.evaluate(&recovered).unwrap(), + Max(Some(if k == 1 { 3 } else { 5 })) + ); + } +} diff --git a/src/unit_tests/rules/maximumdomaticnumber_ilp.rs b/src/unit_tests/rules/maximumdomaticnumber_ilp.rs index 54a6e2e1a..47a49f21c 100644 --- a/src/unit_tests/rules/maximumdomaticnumber_ilp.rs +++ b/src/unit_tests/rules/maximumdomaticnumber_ilp.rs @@ -118,3 +118,18 @@ fn test_maximumdomaticnumber_to_ilp_solution_extraction() { let value = problem.evaluate(&extracted).unwrap(); assert_eq!(value, Max(Some(2))); } + +#[test] +fn test_domatic_normalization_ignores_parallel_edges_and_loops() { + let mut edges = vec![(0, 1); 8]; + edges.extend([(0, 0), (1, 1)]); + let source = MaximumDomaticNumber::new(SimpleGraph::new(2, edges)); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!( + reduction.target_problem().max_constraint_magnitude_bits(), + 1 + ); + let solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!(source.evaluate(&recovered).unwrap(), Max(Some(2))); +} diff --git a/src/unit_tests/rules/maximumsetpacking_ilp.rs b/src/unit_tests/rules/maximumsetpacking_ilp.rs index d8a91d468..5bd83b2d7 100644 --- a/src/unit_tests/rules/maximumsetpacking_ilp.rs +++ b/src/unit_tests/rules/maximumsetpacking_ilp.rs @@ -163,3 +163,18 @@ 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 test_set_packing_normalization_preserves_repeated_membership() { + let source = + MaximumSetPacking::with_weights(vec![vec![0, 0], vec![0], vec![1, 1]], vec![3, 2, 4]) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!(source.evaluate(&recovered).unwrap(), Max(Some(7))); + assert_eq!( + reduction.target_problem().max_constraint_magnitude_bits(), + 1 + ); +} diff --git a/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs b/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs index 1f31f58a5..ae22e673d 100644 --- a/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs +++ b/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs @@ -70,3 +70,29 @@ fn test_minimumcutintoboundedsets_to_ilp_bf_vs_ilp() { ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } + +#[test] +fn test_partition_bound_normalization_preserves_the_optimum() { + for bound in [0, 1, 2, 1000] { + let source = MinimumCutIntoBoundedSets::new( + SimpleGraph::new(2, vec![(0, 1)]), + vec![3_i64], + 0, + 1, + bound, + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + let solution = crate::solvers::ILPSolver::new().solve(reduction.target_problem()); + assert_eq!(solution.is_ok(), bound >= 1); + if let Ok(solution) = solution { + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!(source.evaluate(&recovered).unwrap().0, Some(3)); + } else { + assert_eq!( + solution.unwrap_err(), + crate::solvers::ILPSolveError::Infeasible + ); + } + } +} diff --git a/src/unit_tests/rules/minimumedgecostflow_ilp.rs b/src/unit_tests/rules/minimumedgecostflow_ilp.rs index 7f8eed3cd..99a4d7232 100644 --- a/src/unit_tests/rules/minimumedgecostflow_ilp.rs +++ b/src/unit_tests/rules/minimumedgecostflow_ilp.rs @@ -146,3 +146,33 @@ fn test_minimumedgecostflow_to_ilp_extract_solution() { assert_eq!(extracted, vec![0, 1, 2, 0, 1, 2]); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(3))); } + +#[test] +fn test_flow_requirement_normalization_preserves_feasibility() { + for requirement in [i64::MIN, 0, 1, 2, i64::MAX] { + let source = MinimumEdgeCostFlow::new( + DirectedGraph::new(2, vec![(0, 1)]), + vec![3], + vec![1], + 0, + 1, + requirement, + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(requirement <= 1); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!( + source.evaluate(&recovered).unwrap(), + Min(Some(if requirement == 1 { 3 } else { 0 })) + ); + } + Err(error) => { + assert!(requirement > 1); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/rules/openshopscheduling_ilp.rs b/src/unit_tests/rules/openshopscheduling_ilp.rs index bee202188..f8a4c1641 100644 --- a/src/unit_tests/rules/openshopscheduling_ilp.rs +++ b/src/unit_tests/rules/openshopscheduling_ilp.rs @@ -178,3 +178,30 @@ fn test_openshopscheduling_to_ilp_single_machine() { assert!(value.0.is_some()); assert_eq!(value, Min(Some(6))); } + +#[test] +fn test_decision_makespan_threshold_normalization() { + for bound in [i64::MIN, -1, 0, 1, i64::MAX] { + let source = crate::models::Decision::new(OpenShopScheduling::new(1, vec![vec![1]]), bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!( + reduction.target_problem().max_constraint_magnitude_bits(), + 1 + ); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(bound >= 1); + assert!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap() + .0 + ); + } + Err(error) => { + assert!(bound < 1); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs b/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs index 7076d515b..e23268e8d 100644 --- a/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs +++ b/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs @@ -47,3 +47,30 @@ fn test_pathconstrainednetworkflow_to_ilp_bf_vs_ilp() { ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } + +#[test] +fn test_flow_requirement_normalization_preserves_feasibility() { + for requirement in [i64::MIN, 0, 1, 2, i64::MAX] { + let source = PathConstrainedNetworkFlow::new( + DirectedGraph::new(2, vec![(0, 1)]), + vec![1], + 0, + 1, + vec![vec![0]], + requirement, + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(requirement <= 1); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert!(source.evaluate(&recovered).unwrap()); + } + Err(error) => { + assert!(requirement > 1); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs b/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs index 3f54767c4..e2b54f808 100644 --- a/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs +++ b/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs @@ -90,3 +90,23 @@ fn test_precedenceconstrainedscheduling_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 test_processor_normalization_preserves_scheduling_feasibility() { + for processors in [1, 2, 1000] { + let source = PrecedenceConstrainedScheduling::new(2, processors, 1, vec![]); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + let solution = ILPSolver::new().solve(reduction.target_problem()); + assert_eq!(solution.is_ok(), processors >= 2); + if let Ok(solution) = solution { + let recovered = reduction.extract_solution(&solution).unwrap(); + assert!(source.evaluate(&recovered).unwrap().0); + } else { + assert_eq!( + solution.unwrap_err(), + crate::solvers::ILPSolveError::Infeasible + ); + } + } +} diff --git a/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs b/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs index 056c224de..39920f7a4 100644 --- a/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs +++ b/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs @@ -55,3 +55,30 @@ fn test_rectilinearpicturecompression_to_ilp_trivial() { let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 0); // no maximal rects } + +#[test] +fn test_rectangle_threshold_normalization() { + for bound in [i64::MIN, -1, 0, 1, i64::MAX] { + let source = RectilinearPictureCompression::new(vec![vec![true]], bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!( + reduction.target_problem().max_constraint_magnitude_bits(), + 1 + ); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(bound >= 1); + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Or(true) + ); + } + Err(error) => { + assert!(bound < 1); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs b/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs index b754b7ed2..c276916c7 100644 --- a/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs +++ b/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs @@ -82,3 +82,30 @@ fn test_solution_extraction() { assert_eq!(extracted.len(), 3); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } + +#[test] +fn test_storage_threshold_normalization() { + for n in [0, 2] { + for bound in [i64::MIN, -1, 0, i64::MAX] { + let subsets = if n == 0 { vec![] } else { vec![vec![0, 1]] }; + let source = RootedTreeStorageAssignment::new(n, subsets, bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(bound >= 0); + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Or(true) + ); + } + Err(error) => { + assert!(bound < 0); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } + } +} diff --git a/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs b/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs index 8838a3843..64c85c59f 100644 --- a/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs +++ b/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs @@ -117,3 +117,27 @@ fn test_schedulingwithindividualdeadlines_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 test_individual_deadline_threshold_normalization() { + for processors in [1, 2, 1000] { + let source = SchedulingWithIndividualDeadlines::new(2, processors, vec![1, 1], vec![]); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(processors >= 2); + assert!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap() + .0 + ); + } + Err(error) => { + assert_eq!(processors, 1); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/rules/setsplitting_ilp.rs b/src/unit_tests/rules/setsplitting_ilp.rs index c4cb8d97c..3acc5f16d 100644 --- a/src/unit_tests/rules/setsplitting_ilp.rs +++ b/src/unit_tests/rules/setsplitting_ilp.rs @@ -108,3 +108,32 @@ fn test_overhead_dimensions() { assert_eq!(ilp.num_vars(), 5); assert_eq!(ilp.constraints().len(), 6); // 2 per subset } + +#[test] +fn test_set_splitting_normalization_preserves_all_colorings() { + for subset in [vec![0, 0, 0, 0, 0, 0, 0, 0, 1], vec![0, 0]] { + let source = SetSplitting::new(2, vec![subset]); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + for a in [false, true] { + for b in [false, true] { + let expected = source.evaluate(&vec![a, b]).unwrap().0; + let solution = vec![i64::from(a), i64::from(b)]; + assert_eq!( + reduction + .target_problem() + .evaluate(&solution) + .unwrap() + .is_valid(), + expected + ); + if expected { + assert_eq!(reduction.extract_solution(&solution).unwrap(), vec![a, b]); + } + } + } + assert_eq!( + reduction.target_problem().max_constraint_magnitude_bits(), + 1 + ); + } +} diff --git a/src/unit_tests/rules/timetabledesign_ilp.rs b/src/unit_tests/rules/timetabledesign_ilp.rs index 14fd854ac..a37656505 100644 --- a/src/unit_tests/rules/timetabledesign_ilp.rs +++ b/src/unit_tests/rules/timetabledesign_ilp.rs @@ -88,3 +88,34 @@ fn test_timetabledesign_to_ilp_identity_extraction() { ); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } + +#[test] +fn test_timetable_threshold_normalization() { + for requirement in [i64::MIN, 0, 1, 2, i64::MAX] { + let source = TimetableDesign::new( + 1, + 1, + 1, + vec![vec![true]], + vec![vec![true]], + vec![vec![requirement]], + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!((0..=1).contains(&requirement)); + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Or(true) + ); + } + Err(error) => { + assert!(!(0..=1).contains(&requirement)); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/symbolic_parameter_contracts.rs b/src/unit_tests/symbolic_parameter_contracts.rs index 49826259c..1c506a4e1 100644 --- a/src/unit_tests/symbolic_parameter_contracts.rs +++ b/src/unit_tests/symbolic_parameter_contracts.rs @@ -169,7 +169,23 @@ where .expect("direct reduction is registered"); let contract = entry.parameter_contract().unwrap(); let transform = contract.transform().expect("symbolic transform exists"); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + // Auxiliary fields have their own relations (for example, ILP magnitude + // bounds alongside exact variable counts). Check each against the target. for (field, _) in transform.expressions() { + let predicted = predicted.get(field).unwrap(); + let actual = actual.get(field).unwrap(); + assert!( + match transform.relation(field).unwrap() { + ParameterRelation::Exact => predicted == actual, + ParameterRelation::UpperBound => predicted >= actual, + }, + "{} -> {}: {field}: predicted {predicted}, measured {actual}", + S::NAME, + T::NAME + ); + } + for &field in fields { assert_eq!( transform.relation(field), Some(relation), @@ -177,23 +193,6 @@ where S::NAME, T::NAME ); - } - let predicted = transform.evaluate(&source.parameters()).unwrap(); - if relation == ParameterRelation::Exact { - for (field, _) in transform.expressions() { - if fields.contains(&field) { - continue; - } - assert_eq!( - predicted.get(field), - actual.get(field), - "{} -> {}: {field}", - S::NAME, - T::NAME - ); - } - } - for &field in fields { assert_eq!( predicted.get(field), actual.get(field), @@ -214,7 +213,7 @@ where } #[test] -fn newly_exact_parameters_match_reduced_instances() { +fn parameter_relations_match_reduced_instances() { use crate::models::algebraic::MinimumMatrixCover; use crate::models::algebraic::{ IntegerVariable, LinearConstraint, ObjectiveSense, QuadraticAssignment, BMF, ILP, @@ -266,7 +265,7 @@ fn newly_exact_parameters_match_reduced_instances() { check_reduced_parameters::<_, ILP>( IntegerKnapsack::new(vec![3, 4], vec![5, 6], 7).unwrap(), &["num_nonzeros"], - exact, + ParameterRelation::UpperBound, ); check_reduced_parameters::<_, ILP>( LongestCommonSubsequence::new(2, vec![vec![0, 1], vec![1, 0, 1]]), From b51b699d54d430e4f098b1cf5841fd66e0be04d8 Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:16:38 -0700 Subject: [PATCH 4/5] Remove ILP-specific bounds section from design overview --- docs/src/design.md | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/docs/src/design.md b/docs/src/design.md index 0b231ee58..04eba0f43 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -522,34 +522,6 @@ Big-O display; it does not rank or filter paths. ## Solvers -### ILP bounds variants - -`ILP` separates the variable domain, coefficient type, and bounds -requirement. `B` defaults to `General`: omitted bounds in a user problem -reference resolve to `bounds=general`. General ILP accepts finite and infinite -variable intervals. Binary variables still have `[0, 1]` domains independently -of this dimension. - -`ILP` requires explicit finite endpoints for every variable, -validated by both construction and deserialization. Use `with_variables` to -supply them; no bounds are inferred from constraint rows. It is the only -additional concrete registration. There is no bounded-binary or bounded-float -registration. - -Binary ILP embeds into bounded integer ILP, which embeds into general integer -ILP without removing any stored bounds or changing the objective. Binary -encoding starts only from bounded integer ILP. Incoming rules target binary ILP -when all variables are binary and bounded integer ILP when finite integer -domains are part of their construction. Their bounds must preserve source -feasibility and optima. - -These are mathematical graph edges. Arithmetic overflow remains an execution -error, and backend availability belongs to the separate solver capability -registry. Fixed solver pipelines stop directly at the registered bounded ILP -terminal; they do not need the embedding into general ILP. - -### Solver execution - The reference solver exposes a direct typed operation: ```rust,ignore From 6029c334854a56075c49f4c98a04811ae62392b5 Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Sun, 27 Sep 2026 22:34:17 -0700 Subject: [PATCH 5/5] Replace exponential highly connected deletion reduction with polynomial ILP Encode cluster membership with pair variables and degree constraints. Preserve optimal edge deletion, declare composable polynomial size bounds, and remove obsolete subset-enumeration helpers. Update the proof and verify exhaustive small graphs, decoding, large graph construction, and QUBO recovery. --- docs/paper/reductions.typ | 24 +- problemreductions-cli/tests/cli_tests.rs | 31 +- src/models/graph/highly_connected_deletion.rs | 65 ----- src/rules/highlyconnecteddeletion_ilp.rs | 268 +++++++----------- .../rules/highlyconnecteddeletion_ilp.rs | 248 +++++++++------- 5 files changed, 274 insertions(+), 362 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index adff1e79f..6652302eb 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -15525,18 +15525,24 @@ The following reductions to Integer Linear Programming are straightforward formu Source deletion witness $(#fmt-values(hcd_ilp_sol.source_config))$, target ILP witness $(#fmt-values(hcd_ilp_sol.target_config))$. ], )[ - Enumerate the family of feasible clusters of $G$ and pick a partition of $V$ into feasible clusters maximizing the kept internal edge count; since $|E|$ is fixed, this is equivalent to minimizing deleted edges @HueffnerKomusiewiczLiebtrauNiedermeier2014. + Encode cluster membership with one binary variable per unordered vertex pair. Transitivity and minimum-degree constraints describe a partition into singletons and highly connected clusters, maximizing the number of retained edges. ][ - _Construction._ Let the source instance be a simple undirected graph $G = (V, E)$. Call a vertex set $S subset.eq V$ a _feasible cluster_ when either $|S| = 1$, or $|S| >= 3$ and the induced subgraph $G[S]$ is _highly connected_, i.e. its edge connectivity satisfies $lambda(G[S]) > |S| / 2$ (strict). Let $cal(C)(G)$ be the family of all feasible clusters. Introduce binary variables $x_S in {0, 1}$ for each $S in cal(C)(G)$, where $x_S = 1$ iff $S$ is chosen as one block of the final partition. The ILP is: - $ - max quad & sum_(S in cal(C)(G)) |E(G[S])| x_S \ - "subject to" quad & sum_(S in cal(C)(G), v in S) x_S = 1 quad forall v in V \ - & x_S in {0, 1}. - $ + _Construction._ Let $G = (V, E)$ have $n$ vertices. Introduce symmetric binary variables $y_(u v) = y_(v u)$ for distinct vertices, meaning that $u$ and $v$ belong to the same cluster, and a binary non-singleton flag $a_v$ for each vertex. For every triple, impose all three inequalities of the form + $ y_(u v) + y_(v w) - y_(u w) <= 1. $ + Together with reflexive membership, these constraints define an equivalence relation. Write + $ s_v = sum_(u != v) y_(u v), quad d_v = sum_(u in N(v)) y_(u v), $ + where $N(v)$ contains distinct neighbors other than $v$. Impose + $ s_v <= (n-1) a_v, quad 2 d_v >= s_v + 2 a_v. $ + For $n = 0$, there are no variables or rows. Maximize $sum_({u,v} in E, u != v) y_(u v)$, counting duplicate edges with multiplicity. Self-loops are always retained and contribute only a constant to the retained-edge count. + + _Degree characterization._ A simple graph on $k >= 2$ vertices is highly connected exactly when its minimum degree $delta$ exceeds $k/2$. Necessity follows from $lambda <= delta$. For sufficiency, consider any cut with smaller side of size $b <= k/2$. At least $b(delta-b+1)$ edges cross it. Since $(b-1)(delta-b) >= 0$, this is at least $delta > k/2$. + + _Correctness._ For a singleton, $s_v = d_v = 0$ and the constraints force $a_v = 0$. In a larger cluster of size $k = s_v+1$, the first inequality forces $a_v = 1$, and the second requires $2 d_v >= k+1$. Thus every non-singleton cluster is highly connected by the degree characterization; clusters of size two are excluded automatically. Conversely, every partition into allowed clusters satisfies the constraints with these membership and flag values. Any feasible source deletion can restore all edges internal to its components without decreasing their connectivity or increasing the deletion cost. Hence some source optimum keeps every internal edge, and maximizing the target objective preserves that optimum. + + _Solution extraction._ Delete precisely the non-loop source edges whose membership variable is zero. Keep every self-loop. The resulting components are the encoded clusters, and their connectivity follows from the constraints. - _Correctness._ ($arrow.r.double$) Any feasible source partition $cal(P) = {B_1, dots, B_k}$ -- where every block $B_i$ is a singleton or a highly connected component on $>= 3$ vertices -- yields the feasible ILP assignment $x_(B_i) = 1$ for $i = 1, dots, k$ and $0$ elsewhere; the partition constraints hold because each vertex belongs to exactly one block, and the objective value is the number of edges kept by the partition. ($arrow.l.double$) Any feasible ILP solution selects a sub-family of $cal(C)(G)$ that, by the equality constraints, partitions $V$ into feasible clusters; the objective equals the number of intra-cluster edges. Since $|E|$ is constant, maximizing intra-cluster edges is equivalent to minimizing $|E| - sum_S |E(G[S])| x_S$, the number of deleted edges. + _Size and running time._ The target has $n(n+1)/2$ binary variables, $3 binom(n,3)+2n$ constraints, and $O(n^3)$ nonzeros. Constraint coefficients have magnitude at most $max(n-1,2)$; objective coefficients count input edge multiplicities. Construction, encoding length, and extraction are polynomial in the source encoding size. All registered parameter bounds use only the source vertex count. - _Solution extraction._ Decode the chosen clusters $C subset.eq cal(C)(G)$ from $x$. The source configuration is the binary edge-deletion vector: edge $e = {u, v}$ is kept (config bit $0$) iff some chosen cluster $S in C$ contains both $u$ and $v$, otherwise deleted (config bit $1$). ] #let ep_ilp = load-example( diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 37f2ad929..d60932ca0 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -5603,8 +5603,8 @@ fn test_path_overall_preserves_unavailable_fields_alongside_exact_fields() { let output = pred() .args([ "path", - "HighlyConnectedDeletion", - "ILP/bool", + "IntegralFlowWithMultipliers", + "ILP/i64/i64/bounded", "--limit", "1", "--json", @@ -5626,11 +5626,20 @@ fn test_path_overall_preserves_unavailable_fields_alongside_exact_fields() { }) .collect::>(); assert_eq!(relations["num_constraints"], "exact"); - assert_eq!(relations["num_vars"], "unavailable"); + assert_eq!(relations["num_vars"], "exact"); + assert_eq!(relations["max_constraint_magnitude_bits"], "unavailable"); + let unavailable = fields + .iter() + .find(|field| field["relation"] == "unavailable") + .unwrap(); + assert!(unavailable["reason"] + .as_str() + .unwrap() + .contains("multipliers")); } #[test] -fn test_path_overall_unavailable_reason_explains_unsupported_bound() { +fn test_path_highly_connected_deletion_has_complete_polynomial_predictions() { let output = pred() .args(["path", "HighlyConnectedDeletion", "ILP/bool", "--json"]) .output() @@ -5644,11 +5653,15 @@ fn test_path_overall_unavailable_reason_explains_unsupported_bound() { .map(|field| (field["field"].as_str().unwrap(), field)) .collect::>(); - assert_eq!(fields["num_vars"]["relation"], "unavailable"); - assert!(fields["num_vars"]["reason"] - .as_str() - .unwrap() - .contains("variable exponent unsupported")); + assert_eq!(fields.len(), 4); + assert_eq!(fields["num_vars"]["relation"], "exact"); + for field in [ + "num_constraints", + "num_nonzeros", + "max_constraint_magnitude_bits", + ] { + assert_eq!(fields[field]["relation"], "upper_bound"); + } } #[test] diff --git a/src/models/graph/highly_connected_deletion.rs b/src/models/graph/highly_connected_deletion.rs index d1f7fca64..436eb6f7c 100644 --- a/src/models/graph/highly_connected_deletion.rs +++ b/src/models/graph/highly_connected_deletion.rs @@ -345,71 +345,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec(graph: &G, vertices: &[usize]) -> bool { - let size = vertices.len(); - if size == 0 { - return false; - } - if size == 1 { - return true; - } - if size == 2 { - return false; - } - - // Build induced-subgraph adjacency restricted to `vertices`. - let n = graph.num_vertices(); - let in_subset: HashSet = vertices.iter().copied().collect(); - let mut adj: Vec> = vec![Vec::new(); n]; - for (u, v) in graph.edges() { - if in_subset.contains(&u) && in_subset.contains(&v) { - adj[u].push(v); - adj[v].push(u); - } - } - - // The induced subgraph must itself be connected (a single component). - let mut visited: HashSet = HashSet::new(); - let start = vertices[0]; - let mut queue: VecDeque = VecDeque::new(); - queue.push_back(start); - visited.insert(start); - while let Some(u) = queue.pop_front() { - for &w in &adj[u] { - if !visited.contains(&w) { - visited.insert(w); - queue.push_back(w); - } - } - } - if visited.len() != size { - return false; - } - - // Strict inequality: λ(G[S]) > |S| / 2, equivalently 2 * λ > |S|. - let lambda = edge_connectivity(vertices, &adj); - 2 * lambda > size -} - -/// Count the number of induced edges of `graph` whose endpoints both lie -/// inside `vertices`. -pub(crate) fn induced_edge_count(graph: &G, vertices: &[usize]) -> usize { - let in_subset: HashSet = vertices.iter().copied().collect(); - graph - .edges() - .into_iter() - .filter(|(u, v)| in_subset.contains(u) && in_subset.contains(v)) - .count() -} - #[cfg(test)] #[path = "../../unit_tests/models/graph/highly_connected_deletion.rs"] mod tests; diff --git a/src/rules/highlyconnecteddeletion_ilp.rs b/src/rules/highlyconnecteddeletion_ilp.rs index bceaf7be6..8fd2c2cf2 100644 --- a/src/rules/highlyconnecteddeletion_ilp.rs +++ b/src/rules/highlyconnecteddeletion_ilp.rs @@ -1,50 +1,34 @@ -//! Reduction from HighlyConnectedDeletion to ILP (Integer Linear Programming). +//! Polynomial reduction from HighlyConnectedDeletion to binary ILP. //! -//! Encodes the set-partitioning ILP of Hüffner, Komusiewicz, Liebtrau, and -//! Niedermeier (IEEE/ACM TCBB 2014). Given a simple undirected graph -//! `G = (V, E)`: +//! One variable per unordered vertex pair records membership in the same cluster. +//! Triangle inequalities make membership transitive. A binary flag per vertex +//! distinguishes singleton clusters, and linear degree constraints enforce +//! minimum degree strictly greater than half the cluster size otherwise. //! -//! - Enumerate the family `C(G)` of *feasible clusters*: every singleton plus -//! every subset `S` with `|S| >= 3` whose induced subgraph `G[S]` is highly -//! connected (edge connectivity strictly greater than `|S| / 2`). -//! - Introduce a binary variable `x_S` per feasible cluster (1 iff `S` is one -//! block of the chosen partition). -//! - Partition constraints: for every vertex `v`, -//! `sum_{S in C(G), v in S} x_S = 1`. -//! - Maximize the number of kept (intra-cluster) edges: -//! `max sum_{S in C(G)} |E(G[S])| * x_S`. +//! This degree condition is equivalent to high edge connectivity: for minimum +//! degree d > k/2, a cut with smaller side a <= k/2 has at least +//! a*(d-a+1) >= d edges. Conversely, edge connectivity never exceeds minimum +//! degree. See . //! -//! Because `|E|` is fixed, maximizing kept internal edges minimizes deleted -//! edges; the source value is recovered as -//! `deleted_edges = |E| - ilp_objective`. -//! -//! Reference: Falk Hüffner, Christian Komusiewicz, Adrian Liebtrau, and Rolf -//! Niedermeier, "Partitioning Biological Networks into Highly Connected -//! Clusters with Maximum Edge Coverage," IEEE/ACM Transactions on -//! Computational Biology and Bioinformatics 11(3):455–467, 2014. -//! +//! Maximize the number of non-loop edges kept inside clusters. Any feasible +//! deletion can restore all internal edges without losing connectivity, so an +//! optimal source solution is represented. Self-loops are always kept. use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; -use crate::models::graph::highly_connected_deletion::{induced_edge_count, is_feasible_cluster}; use crate::models::graph::HighlyConnectedDeletion; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::{Graph, SimpleGraph}; -/// Result of reducing HighlyConnectedDeletion to ILP. +/// Result of reducing HighlyConnectedDeletion to binary ILP. /// -/// Variable layout (all binary): -/// - `x_S` at index `c` is the indicator for the `c`-th feasible cluster -/// stored in `clusters`. Indices follow the enumeration order produced by -/// [`enumerate_feasible_clusters`], which always lists every singleton first -/// followed by larger feasible clusters in subset-id order. +/// Variables are unordered pairs in lexicographic order, followed by one +/// non-singleton flag per vertex. #[derive(Debug, Clone)] pub struct ReductionHighlyConnectedDeletionToILP { target: ILP, - /// Feasible clusters in variable order; `clusters[c]` is sorted ascending. - clusters: Vec>, - /// Source graph edges in the same order as `source.graph().edges()`. - edges: Vec<(usize, usize)>, + /// Pair variable for each source edge; self-loops have no variable. + edge_variables: Vec>, } impl ReductionResult for ReductionHighlyConnectedDeletionToILP { @@ -55,166 +39,110 @@ impl ReductionResult for ReductionHighlyConnectedDeletionToILP { &self.target } - /// Decode a binary ILP assignment into the source's edge-deletion config. - /// - /// For every source edge `(u, v)`, the edge is *kept* iff some chosen - /// cluster `S` (i.e. with `x_S = 1`) contains both `u` and `v`; otherwise - /// it is deleted (`config[e] = 1`). fn extract_solution( &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" - ))); - } - + crate::rules::traits::validate_target_witness( + self.target_problem(), + target_solution, + |value| value.value.is_some(), + "target ILP assignment is infeasible", + )?; Ok(self - .edges + .edge_variables .iter() - .map(|&(u, v)| cluster_of[u] != cluster_of[v]) + .map(|variable| variable.is_some_and(|index| target_solution[index] == 0)) .collect()) } } -/// Number of source vertices, recovered from the clusters list. -/// -/// The reduction always enumerates the `n` singletons first, so any vertex id -/// occurring anywhere in `clusters` is `< n`. We read `n` off the singletons -/// for clarity and robustness. -fn vertex_count(clusters: &[Vec]) -> usize { - clusters - .iter() - .filter(|c| c.len() == 1) - .map(|c| c[0] + 1) - .max() - .unwrap_or(0) -} - -/// Enumerate every feasible cluster of `graph` in deterministic order. -/// -/// 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, -) -> Result>, crate::rules::ReductionError> { - let n = graph.num_vertices(); - 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. - for v in 0..n { - clusters.push(vec![v]); - } - - if n < 3 { - return Ok(clusters); - } - - // Larger feasible clusters by ascending subset bitmask. - for mask in 1u64..(1u64 << n) { - let popcount = mask.count_ones() as usize; - if popcount < 3 { - continue; - } - let subset: Vec = (0..n).filter(|v| (mask >> v) & 1 == 1).collect(); - if is_feasible_cluster(graph, &subset) { - clusters.push(subset); - } - } - - Ok(clusters) -} - -#[reduction( - transform = exact { - max_constraint_magnitude_bits = "1", - num_constraints = "num_vertices", +// Three rows per vertex triple and two per vertex. Triple rows have three +// nonzeros; each vertex row has at most n. All row magnitudes are <= max(n-1, 2). +#[reduction(transform = { + exact { + num_vars = "num_vertices * (num_vertices + 1) / 2", }, - unavailable = { - num_vars = "the feasible-cluster count depends on graph structure, and its 2^num_vertices upper bound requires a variable exponent unsupported by the size-transform evaluator", - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", -} -)] + upper_bound { + num_constraints = "num_vertices^3 + 2 * num_vertices", + num_nonzeros = "3 * num_vertices^3 + 2 * num_vertices^2", + max_constraint_magnitude_bits = "num_vertices + 2", + }, +})] impl ReduceTo> for HighlyConnectedDeletion { type Result = ReductionHighlyConnectedDeletionToILP; fn reduce_to(&self) -> Result { let graph = self.graph(); let n = graph.num_vertices(); - 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. - // Each constraint is built by scanning the cluster list once per - // vertex; total work is O(n * sum |S|) which stays tractable for the - // small graphs we use in tests. - let mut constraints: Vec = Vec::with_capacity(n); - for v in 0..n { - let terms: Vec<(usize, i64)> = clusters - .iter() - .enumerate() - .filter_map(|(c, cluster)| { - if cluster.binary_search(&v).is_ok() { - Some((c, 1)) - } else { - None + let num_pairs = n.checked_mul(n.saturating_sub(1)).map(|value| value / 2); + let num_vars = num_pairs + .and_then(|pairs| pairs.checked_add(n)) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::>( + "counting pair and non-singleton variables", + ) + })?; + let flag_offset = num_vars - n; + let max_companions = + Self::exact_i64(n.saturating_sub(1), "encoding the cluster-size bound")?; + // Lexicographic pair index: preceding row lengths, then the column offset. + // The checked n*(n-1) above also bounds these products for u < v < n. + let pair = |u: usize, v: usize| { + let (u, v) = (u.min(v), u.max(v)); + u * n - u * (u + 1) / 2 + (v - u - 1) + }; + let mut constraints = Vec::new(); + for u in 0..n { + for v in u + 1..n { + for w in v + 1..n { + let (a, b, c) = (pair(u, v), pair(u, w), pair(v, w)); + for (first, second, third) in [(a, b, c), (a, c, b), (b, c, a)] { + constraints.push(LinearConstraint::le( + vec![(first, 1), (second, 1), (third, -1)], + 1, + )); } - }) - .collect(); - constraints.push(LinearConstraint::eq(terms, 1)); + } + } } - - // Objective: maximize sum_S |E(G[S])| * x_S. - let objective: Vec<(usize, i64)> = clusters + for v in 0..n { + // s_v is the number of other vertices in v's cluster. The flag + // a_v is forced on for s_v > 0 by s_v <= (n-1)*a_v. + let mut size_terms = Vec::new(); + let mut degree_terms = Vec::new(); + for u in 0..n { + if u != v { + let variable = pair(u, v); + size_terms.push((variable, 1)); + // 2*d_v - s_v: count each distinct, non-loop neighbor once. + degree_terms.push((variable, if graph.has_edge(u, v) { 1 } else { -1 })); + } + } + size_terms.push((flag_offset + v, -max_companions)); + constraints.push(LinearConstraint::le(size_terms, 0)); + // 2*d_v >= s_v + 2*a_v: singleton clusters pass, while other + // clusters require 2*d_v > s_v+1 and automatically exclude pairs. + degree_terms.push((flag_offset + v, -2)); + constraints.push(LinearConstraint::ge(degree_terms, 0)); + } + let edge_variables: Vec<_> = graph + .edges() .iter() - .enumerate() - .map(|(c, cluster)| { - i64::try_from(induced_edge_count(graph, cluster)) - .map(|count| (c, count)) - .map_err(|_| { - crate::rules::ReductionError::integer_overflow::< - HighlyConnectedDeletion, - ILP, - >("encoding an induced edge count") - }) - }) - .collect::>()?; - + .map(|&(u, v)| (u != v).then(|| pair(u, v))) + .collect(); + // Duplicate edges contribute their multiplicity to the objective; + // self-loops are constant and need no objective term. + let objective = edge_variables + .iter() + .flatten() + .map(|&variable| (variable, 1)) + .collect(); let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Maximize) .map_err(Self::target_construction)?; - Ok(ReductionHighlyConnectedDeletionToILP { target, - clusters, - edges: graph.edges(), + edge_variables, }) } } @@ -224,7 +152,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source).unwrap(); - assert_eq!(reduction.target_problem().num_vars(), 2); - assert_bf_vs_ilp(&source, &reduction); -} -use crate::models::algebraic::{ObjectiveSense, ILP}; -use crate::models::graph::HighlyConnectedDeletion; +use crate::models::algebraic::QUBO; use crate::rules::test_helpers::assert_bf_vs_ilp; -use crate::topology::SimpleGraph; +use crate::rules::{ReductionGraph, ReductionPath, ReductionStep}; +use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; 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 { +fn triangle_with_leaf() -> HighlyConnectedDeletion { HighlyConnectedDeletion::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)])) } #[test] -fn test_highlyconnecteddeletion_to_ilp_issue_structure() { - let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - let ilp = reduction.target_problem(); - - // 4 singletons + the triangle cluster {0,1,2}: 5 variables in total. - assert_eq!(ilp.num_vars(), 5); - assert_eq!(ilp.constraints().len(), 4); - assert_eq!(ilp.sense(), ObjectiveSense::Maximize); - - // The induced-edge counts: singletons contribute 0, triangle contributes 3. - let triangle_coeffs: Vec = ilp - .objective() - .iter() - .filter(|(_, w)| *w > 0) - .map(|(_, w)| *w) - .collect(); - assert_eq!(triangle_coeffs, vec![3]); - - // Vertex 3 only appears in its own singleton, so its partition constraint - // is `x_{3} = 1` -- a single-term equality with rhs 1. - let v3_constraint = &ilp.constraints()[3]; - assert_eq!(v3_constraint.terms().len(), 1); - assert_eq!(v3_constraint.rhs(), 1); - - // Vertex 0 appears in two clusters (its singleton and the triangle). - let v0_constraint = &ilp.constraints()[0]; - assert_eq!(v0_constraint.terms().len(), 2); - assert_eq!(v0_constraint.rhs(), 1); +fn polynomial_encoding_preserves_small_graph_optima_and_witnesses() { + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| { + entry.source_name == HighlyConnectedDeletion::::NAME + && entry.target_name == ILP::::NAME + }) + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let transform = contract.transform().unwrap(); + for n in 0..=4 { + let pairs: Vec<_> = (0..n) + .flat_map(|u| (u + 1..n).map(move |v| (u, v))) + .collect(); + for graph_mask in 0..1_usize << pairs.len() { + let edges = pairs + .iter() + .enumerate() + .filter_map(|(i, &edge)| (graph_mask & (1 << i) != 0).then_some(edge)) + .collect(); + let source = HighlyConnectedDeletion::new(SimpleGraph::new(n, edges)); + let reduction = source.reduce_to().unwrap(); + let target = reduction.target_problem(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + for (field, actual) in target.parameters().iter() { + assert!(predicted.get(field).expect("complete polynomial contract") >= actual); + } + let reference = BruteForce::new().solve(&source).unwrap().unwrap(); + let expected = source.evaluate(&reference).unwrap(); + let mut best_deleted = None; + for mask in 0..1_usize << target.num_vars() { + let solution: Vec = (0..target.num_vars()) + .map(|i| ((mask >> i) & 1) as i64) + .collect(); + if target.is_feasible(&solution).unwrap() { + let recovered = reduction.extract_solution(&solution).unwrap(); + let deleted = source + .evaluate(&recovered) + .unwrap() + .0 + .expect("decoded witness must be feasible"); + // For a loop-free graph the ILP objective counts every kept edge. + assert_eq!( + target.evaluate_objective(&solution).unwrap() + deleted, + source.num_edges() as i64 + ); + best_deleted = + Some(best_deleted.map_or(deleted, |best: i64| best.min(deleted))); + } + } + assert_eq!(Min(best_deleted), expected, "n={n}, graph={graph_mask}"); + } + } } #[test] -fn test_highlyconnecteddeletion_to_ilp_closed_loop() { - let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert_bf_vs_ilp(&source, &reduction); -} - -#[test] -fn test_highlyconnecteddeletion_to_ilp_bf_vs_ilp() { - let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert_bf_vs_ilp(&source, &reduction); -} - -#[test] -fn test_highlyconnecteddeletion_to_ilp_extract_solution_decode() { - let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - - // ILP solution: pick triangle cluster {0,1,2} and singleton {3}. - // The triangle cluster is the last variable (index 4); singleton {3} is - // index 3. Build the assignment directly. - let mut target_solution = vec![0; reduction.target_problem().num_vars()]; - target_solution[3] = 1; // singleton {3} - target_solution[4] = 1; // triangle {0,1,2} - - let extracted = reduction.extract_solution(&target_solution).unwrap(); - - // Edges in input order: (0,1), (0,2), (1,2) all inside the triangle (kept); - // (2,3) crosses clusters and is deleted. - assert_eq!(extracted, vec![false, false, false, true]); - assert_eq!(source.evaluate(&extracted).unwrap(), Min(Some(1))); - assert!(source.is_valid_solution(&extracted)); +fn polynomial_encoding_handles_more_than_a_word_of_vertices() { + let source = HighlyConnectedDeletion::new(SimpleGraph::new(64, vec![])); + let reduction = source.reduce_to().unwrap(); + let target = reduction.target_problem(); + assert!(target.num_vars() <= 64 * 64); + assert!(target.num_constraints() <= 64_usize.pow(3) + 2 * 64); + let recovered = reduction + .extract_solution(&vec![0; target.num_vars()]) + .unwrap(); + assert_eq!(source.evaluate(&recovered).unwrap(), Min(Some(0))); } #[test] -fn test_highlyconnecteddeletion_to_ilp_rejects_unassigned_vertex() { - let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - let target_solution = vec![0; reduction.target_problem().num_vars()]; - +fn pair_encoding_decodes_clusters_and_rejects_invalid_assignments() { + let source = triangle_with_leaf(); + let reduction = source.reduce_to().unwrap(); + // Pair variables: 01, 02, 03, 12, 13, 23; then four non-singleton flags. + let valid = vec![1, 1, 0, 1, 0, 0, 1, 1, 1, 0]; assert_eq!( - reduction - .extract_solution(&target_solution) - .unwrap_err() - .to_string(), - "vertex 0 has no selected cluster" + reduction.extract_solution(&valid).unwrap(), + vec![false, false, false, true] ); + for invalid in [ + vec![0; 9], + vec![2; 10], + vec![-1; 10], + vec![1; 10], // The leaf prevents the whole graph from being highly connected. + vec![1, 0, 0, 1, 0, 0, 1, 1, 1, 0], // Non-transitive membership. + ] { + assert!(reduction.extract_solution(&invalid).is_err()); + } } #[test] -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. +fn polynomial_encoding_preserves_parallel_edge_costs_and_self_loops() { let source = HighlyConnectedDeletion::new(SimpleGraph::new( - 6, + 4, vec![ - // Triangle on {0,1,2}. + (0, 0), + (0, 1), (0, 1), (0, 2), (1, 2), - // Triangle on {3,4,5}. - (3, 4), - (3, 5), - (4, 5), - // Bridge edge. (2, 3), + (2, 3), + (3, 3), ], )); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - let ilp = reduction.target_problem(); - - // No cluster of size >= 3 may straddle the bridge (the only sets {2,3} or - // any 4+ subsets crossing it fail edge-connectivity). The two triangles - // are feasible; mixed 4-vertex sets are not. - assert_eq!(ilp.sense(), ObjectiveSense::Maximize); - let large_cluster_count = ilp.objective().iter().filter(|(_, w)| *w > 0).count(); - assert_eq!(large_cluster_count, 2); + let reduction = source.reduce_to().unwrap(); + let solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!( + recovered, + vec![false, false, false, false, false, true, true, false] + ); + assert_eq!(source.evaluate(&recovered).unwrap(), Min(Some(2))); +} +#[test] +fn test_highlyconnecteddeletion_to_ilp_closed_loop() { + let source = HighlyConnectedDeletion::new(SimpleGraph::new( + 6, + vec![(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)], + )); + let reduction = source.reduce_to().unwrap(); assert_bf_vs_ilp(&source, &reduction); } + #[test] -fn test_highly_connected_deletion_rejects_mask_overflow() { - let source = HighlyConnectedDeletion::new(SimpleGraph::new(64, vec![])); - assert!( - as ReduceTo>>::reduce_to(&source).is_err() - ); +fn polynomial_size_predictions_and_recovery_work_through_qubo() { + let source = triangle_with_leaf(); + let graph = ReductionGraph::new(); + let path = ReductionPath { + steps: [ + ( + HighlyConnectedDeletion::::NAME, + HighlyConnectedDeletion::::variant(), + ), + (ILP::::NAME, ILP::::variant()), + (QUBO::::NAME, QUBO::::variant()), + ] + .into_iter() + .map(|(name, variant)| ReductionStep { + name: name.into(), + variant: ReductionGraph::variant_to_map(&variant), + }) + .collect(), + }; + let predicted = graph + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap() + .evaluate(&source.parameters()) + .unwrap(); + let chain = graph.reduce_along_path(&path, &source).unwrap().unwrap(); + let qubo = chain.target_problem::>(); + for (field, actual) in qubo.parameters().iter() { + assert!(predicted.get(field).expect("composed polynomial bound") >= actual); + } + let solution = ILPSolver::new().solve(qubo).unwrap(); + let recovered: Vec = chain.extract_solution(&solution).unwrap(); + assert_eq!(source.evaluate(&recovered).unwrap(), Min(Some(1))); }