diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index d44c1be19..28ed6ed64 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -228,7 +228,7 @@ fields to issue templates. Changes to issue templates require user approval. ### Paper (docs/paper/reductions.typ) - `problem-def(name)[def][body]` — defines a problem with auto-generated schema, reductions list, and label ``. Title comes from `display-name` dict. - `reduction-rule(source, target, example: bool, ...)[rule][proof]` — generates a theorem with label `` and registers in `covered-rules` state. Overhead auto-derived from JSON edge data. -- Every directed reduction needs its own `reduction-rule` entry (except trivial Decision↔Optimization pairs which are auto-filtered) +- Every directed reduction needs its own `reduction-rule` entry (except trivial Decision↔Optimization pairs and pure `One → i64 → f64` numeric casts, which are auto-filtered) - Completeness warnings auto-check that all JSON graph nodes/edges are covered in the paper; `Decision

↔ P` edges are excluded since they are trivial solve-and-compare reductions - `display-name` dict maps `ProblemName` to display text @@ -302,7 +302,7 @@ Also add to the `display-name` dictionary: ] ``` -Every directed reduction in the graph needs its own `reduction-rule` entry. The paper auto-checks completeness against the generated `reduction_graph.json` export. +Every nontrivial directed reduction in the graph needs its own `reduction-rule` entry. Do not add paper explanations for pure `One → i64 → f64` numeric casts. Other same-model variant reductions, such as graph transformations and bounded-integer encodings, still require entries. The paper auto-checks completeness against the generated `reduction_graph.json` export. ## Complexity Verification Requirements diff --git a/.claude/skills/how-to-code/SKILL.md b/.claude/skills/how-to-code/SKILL.md index a9827ea9a..33f688a6a 100644 --- a/.claude/skills/how-to-code/SKILL.md +++ b/.claude/skills/how-to-code/SKILL.md @@ -95,7 +95,7 @@ Traits and helpers: `src/rules/traits.rs`, `src/rules/test_helpers.rs`, `src/rul and parameter counts vs the transform (reuse existing inputs; the shared executable-rule check lives in `src/unit_tests/parameter_formula_validation.rs`), and one test per malformed representation the decoder rejects (zero/multiple one-hot bits, duplicate permutation entries, ...). Aggregate edges: test `extract_value` against `BruteForce::solve` on both sides. -5. Paper `reduction-rule` entry — how-to-write-manual (adapt the how-to-verify proof, don't rewrite). +5. Paper `reduction-rule` entry — how-to-write-manual (adapt the how-to-verify proof, don't rewrite). Pure `One -> i64 -> f64` numeric casts are exempt; other same-model variant reductions still need entries. ## Traps diff --git a/.claude/skills/how-to-write-manual/SKILL.md b/.claude/skills/how-to-write-manual/SKILL.md index 21c17dfa1..d904a5ecb 100644 --- a/.claude/skills/how-to-write-manual/SKILL.md +++ b/.claude/skills/how-to-write-manual/SKILL.md @@ -24,8 +24,8 @@ The paper `docs/paper/reductions.typ` is the mathematical manual: one `problem-d - `reduction-rule(source, target, example:, example-source-variant:, example-target-variant:, example-caption:, extra:)[statement][proof]` auto-derives overhead from the graph export and registers the edge for the completeness check; label ``. -- Every directed reduction needs its own entry, including reverses. Only `Decision

<-> P` pairs are - exempt. The completeness warning block at the end of the paper lists missing edges; your change +- Every directed reduction needs its own entry, including reverses. `Decision

<-> P` pairs and pure `One -> i64 -> f64` numeric casts are + exempt; do not explain numeric casts in the paper. Nontrivial same-model variant reductions still require entries. The completeness warning block at the end of the paper lists missing edges; your change must never increase it. ## Example data — never invent diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 44256b45b..92ecbf4d9 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -3287,7 +3287,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] } #{ - let x = load-model-example("SteinerTree") + let x = load-model-example("SteinerTree", variant: (graph: "SimpleGraph", weight: "i64")) let nv = graph-num-vertices(x.instance) let ne = graph-num-edges(x.instance) let edges = x.instance.graph.edges @@ -4451,7 +4451,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| } #{ - let x = load-model-example("ComparativeContainment") + let x = load-model-example("ComparativeContainment", variant: (weight: "i64")) let n = x.instance.universe_size let R = x.instance.r_sets let S = x.instance.s_sets @@ -4994,7 +4994,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #problem-def("QUBO")[ Given $n$ binary variables $x_i in {0, 1}$ and an upper-triangular matrix $Q$, minimize $f(bold(x)) = sum_(i=1)^n Q_(i i) x_i + sum_(i < j) Q_(i j) x_i x_j$ (using $x_i^2 = x_i$ for binary variables). The registered variants use either exact integer coefficients $Q in ZZ^(n times n)$ (the default) or finite floating-point coefficients $Q in RR^(n times n)$. ][ - Equivalent to the Ising model via the linear substitution $s_i = 2x_i - 1$. The native formulation for quantum annealing hardware (e.g., D-Wave) and a standard target for penalty-method reductions @glover2019. QUBO unifies many combinatorial problems into a single unconstrained binary framework, making it a universal intermediate representation for quantum and classical optimization. The integer variant stores exact coefficients, the floating-point variant stores finite real coefficients, and their explicit reduction converts exactly representable integers through `i64_to_exact_f64`. The best known general algorithm runs in $O^*(2^n)$ by brute-force enumeration#footnote[QUBO inherits the Ising model's complexity; no algorithm improving on brute-force is known for the general case.]. + Equivalent to the Ising model via the linear substitution $s_i = 2x_i - 1$. The native formulation for quantum annealing hardware (e.g., D-Wave) and a standard target for penalty-method reductions @glover2019. QUBO unifies many combinatorial problems into a single unconstrained binary framework, making it a universal intermediate representation for quantum and classical optimization. The integer variant stores exact coefficients, the floating-point variant stores finite real coefficients. The best known general algorithm runs in $O^*(2^n)$ by brute-force enumeration#footnote[QUBO inherits the Ising model's complexity; no algorithm improving on brute-force is known for the general case.]. *Example.* Consider $n = #n$ with $Q = mat(#mat-rows)$. The objective is $f(bold(x)) = -x_1 - x_2 - x_3 + 2x_1 x_2 + 2x_2 x_3$. Evaluating all $2^#n$ assignments: $f(0,0,0) = 0$, $f(1,0,0) = -1$, $f(0,1,0) = -1$, $f(0,0,1) = -1$, $f(1,1,0) = 0$, $f(0,1,1) = 0$, $f(1,0,1) = -2$, $f(1,1,1) = 1$. The minimum is $f^* = #fstar$ at $bold(x)^* = (#fmt-values(xstar))$: selecting #selected.join(" and ") avoids the penalty terms #unselected-pairs.join(" and "). @@ -16827,15 +16827,14 @@ See #link("https://github.com/CodingThrust/problem-reductions/blob/main/examples Problems parameterized by graph type, weight type, target type, or clause width ($k$) use explicit reductions between registered variants. Each rule constructs the target representation, registers an exact size map, and preserves the witness representation. #reduction-rule("MaximumIndependentSet", "MaximumIndependentSet")[ - Explicit MIS variant reductions convert King's and triangular lattice graphs to unit-disk graphs, unit-disk graphs to simple adjacency graphs, and unit weights to integer weights. Every rule preserves the vertex indices and has an exact identity size map. + Explicit MIS variant reductions convert King's and triangular lattice graphs to unit-disk graphs, and unit-disk graphs to simple adjacency graphs. Every rule preserves the vertex indices and has an exact identity size map. ][ _Construction._ The registered rules use these concrete maps: - _KingsSubgraph $arrow.r$ UnitDiskGraph:_ integer grid positions $(i, j)$ map to float coordinates with radius $r = 1.5$. - _TriangularSubgraph $arrow.r$ UnitDiskGraph:_ triangular lattice positions map to float coordinates with radius $r = 1.1$. - _UnitDiskGraph $arrow.r$ SimpleGraph:_ discard coordinates, retain only the adjacency edge list. - - _One $arrow.r$ i64:_ each unit weight maps to $1_ZZ$. - _Correctness._ Each graph conversion preserves the adjacency relation, and the weight conversion preserves every unit objective contribution as the integer one. Since the MIS objective $max sum_(v in S) w(v)$ depends only on adjacency and weights, feasible sets and their objective values are preserved. + _Correctness._ Each graph conversion preserves the adjacency relation. Since the MIS objective $max sum_(v in S) w(v)$ depends only on adjacency and weights, feasible sets and their objective values are preserved. _Solution extraction._ Return the target configuration unchanged (identity map on vertices). ] @@ -16860,44 +16859,27 @@ Problems parameterized by graph type, weight type, target type, or clause width _Solution extraction._ Return the target configuration unchanged. ] -#reduction-rule("SpinGlass", "SpinGlass")[ - An Ising spin-glass instance with integer couplings and fields ($J_(i j), h_i in ZZ$) converts to the floating-point variant ($J_(i j), h_i in RR$) through exact `i64_to_exact_f64` embeddings. The graph topology is preserved. -][ - _Construction._ Given $"SpinGlass"(G, bold(J), bold(h))$ with $J_(i j) in ZZ$ and $h_i in ZZ$, construct $"SpinGlass"(G, bold(J)', bold(h)')$ with $J'_(i j) = J_(i j) in RR$ and $h'_i = h_i in RR$. - - _Correctness._ The spin-glass Hamiltonian $H(bold(s)) = sum_((i,j) in E) J_(i j) s_i s_j + sum_i h_i s_i$ is preserved exactly under the integer-to-float embedding (no rounding). Spin configurations and the objective value are unchanged. - - _Solution extraction._ Return the target configuration unchanged. -] - -#reduction-rule("MaximumSetPacking", "MaximumSetPacking")[ - A Maximum Set Packing instance converts unit weights to integer weights ($"One" arrow.r ZZ$) and integer weights to exactly represented floating weights ($ZZ arrow.r RR$). The set family and universe are preserved. -][ - _Construction._ Given $"MSP"(cal(S), bold(w))$, construct $"MSP"(cal(S), bold(w)')$ using: - - _One $arrow.r$ i64:_ each unit weight maps to $1_ZZ$. - - _i64 $arrow.r$ f64:_ each integer weight maps through `i64_to_exact_f64`. - - _Correctness._ The packing constraint (no two selected sets share a universe element) depends only on set membership, not on weights. The objective $max sum_(i in P) w_i$ is preserved under the type embedding. Optimality is unchanged. - - _Solution extraction._ Return the target configuration unchanged. -] - - -#reduction-rule("QUBO", "QUBO")[ - An integer QUBO converts to the floating-coefficient variant by embedding every matrix coefficient with `i64_to_exact_f64`. -][ - _Construction._ Given $Q in ZZ^(n times n)$, construct $Q' in RR^(n times n)$ with $Q'_(i j) = "f64"(Q_(i j))$ for every exactly representable coefficient $|Q_(i j)| lt.eq 2^53 - 1$. - - _Correctness._ For every binary vector $bold(x)$, exact coefficient conversion gives $bold(x)^top Q' bold(x) = bold(x)^top Q bold(x)$. The objective ordering and minimizers are preserved. - - _Solution extraction._ Return the binary vector unchanged. -] // Completeness check: warn about reduction rules in JSON but missing from paper #context { let covered = covered-rules.final() let json-edges = { - let edges = graph-data.edges.map(e => (graph-data.nodes.at(e.source).name, graph-data.nodes.at(e.target).name)) + let is-numeric-cast(src, tgt) = { + src.name == tgt.name and ("weight", "coefficient").any(field => { + let from = src.variant.at(field, default: none) + let to = tgt.variant.at(field, default: none) + let promotion = (from == "One" and to == "i64") or (from == "i64" and to == "f64") + let promoted = src.variant + promoted.insert(field, to) + promotion and promoted == tgt.variant + }) + } + let source = (name: "MaximumIndependentSet", variant: (graph: "SimpleGraph", weight: "One")) + assert(is-numeric-cast(source, (name: source.name, variant: (graph: "SimpleGraph", weight: "i64")))) + assert(not is-numeric-cast(source, (name: source.name, variant: (graph: "TriangularSubgraph", weight: "i64")))) + let edges = graph-data.edges.filter(e => { + not is-numeric-cast(graph-data.nodes.at(e.source), graph-data.nodes.at(e.target)) + }).map(e => (graph-data.nodes.at(e.source).name, graph-data.nodes.at(e.target).name)) let unique = () for e in edges { if unique.find(u => u.at(0) == e.at(0) and u.at(1) == e.at(1)) == none { @@ -17149,13 +17131,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. 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. + ILP variants convert between binary and bounded integer variable domains. 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. ][ - _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. + _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. - _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. + _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. - _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)$. + _Solution extraction._ Binary-to-bounded-integer and bounded-to-general conversions preserve the assignment. 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/src/models/graph/steiner_tree.rs b/src/models/graph/steiner_tree.rs index 585174952..014f28077 100644 --- a/src/models/graph/steiner_tree.rs +++ b/src/models/graph/steiner_tree.rs @@ -365,19 +365,30 @@ crate::register_brute_force! { #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { - vec![crate::example_db::specs::ModelExampleSpec { - id: "steiner_tree_simplegraph", - instance: Box::new(SteinerTree::new( - SimpleGraph::new( - 5, - vec![(0, 1), (0, 3), (1, 2), (1, 3), (2, 3), (2, 4), (3, 4)], - ), - vec![2, 5, 2, 1, 5, 6, 1], - vec![0, 2, 4], - )), - optimal_config: serde_json::json!(vec![true, false, true, true, false, false, true]), - optimal_value: serde_json::json!(6), - }] + vec![ + crate::example_db::specs::ModelExampleSpec { + id: "steiner_tree_simplegraph", + instance: Box::new(SteinerTree::new( + SimpleGraph::new( + 5, + vec![(0, 1), (0, 3), (1, 2), (1, 3), (2, 3), (2, 4), (3, 4)], + ), + vec![2, 5, 2, 1, 5, 6, 1], + vec![0, 2, 4], + )), + optimal_config: serde_json::json!(vec![true, false, true, true, false, false, true]), + optimal_value: serde_json::json!(6), + }, + crate::example_db::specs::ModelExampleSpec { + id: "steiner_tree_one", + instance: Box::new(SteinerTree::::unit_weights( + SimpleGraph::path(3), + vec![0, 2], + )), + optimal_config: serde_json::json!([true, true]), + optimal_value: serde_json::json!(2), + }, + ] } #[cfg(test)] diff --git a/src/models/set/comparative_containment.rs b/src/models/set/comparative_containment.rs index f628c9744..f03046481 100644 --- a/src/models/set/comparative_containment.rs +++ b/src/models/set/comparative_containment.rs @@ -366,21 +366,31 @@ fn contains_selected_subset_unchecked(config: &[bool], set: &[usize]) -> bool { #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { - vec![crate::example_db::specs::ModelExampleSpec { - id: "comparative_containment", - instance: Box::new( - ComparativeContainment::with_weights( - 4, - vec![vec![0, 1, 2, 3], vec![0, 1]], - vec![vec![0, 1, 2, 3], vec![2, 3]], - vec![2, 5], - vec![3, 6], - ) - .expect("canonical comparative-containment instance must be valid"), - ), - optimal_config: serde_json::json!(vec![false, true, false, false]), - optimal_value: serde_json::json!(true), - }] + vec![ + crate::example_db::specs::ModelExampleSpec { + id: "comparative_containment", + instance: Box::new( + ComparativeContainment::with_weights( + 4, + vec![vec![0, 1, 2, 3], vec![0, 1]], + vec![vec![0, 1, 2, 3], vec![2, 3]], + vec![2, 5], + vec![3, 6], + ) + .expect("canonical comparative-containment instance must be valid"), + ), + optimal_config: serde_json::json!(vec![false, true, false, false]), + optimal_value: serde_json::json!(true), + }, + crate::example_db::specs::ModelExampleSpec { + id: "comparative_containment_one", + instance: Box::new( + ComparativeContainment::::new(2, vec![vec![0]], vec![vec![1]]).unwrap(), + ), + optimal_config: serde_json::json!([true, false]), + optimal_value: serde_json::json!(true), + }, + ] } #[cfg(test)] diff --git a/src/rules/binpacking_casts.rs b/src/rules/binpacking_casts.rs new file mode 100644 index 000000000..bfeccb10b --- /dev/null +++ b/src/rules/binpacking_casts.rs @@ -0,0 +1,21 @@ +//! Numeric variant reductions for BinPacking. + +use crate::impl_variant_reduction; +use crate::models::misc::BinPacking; +use crate::rules::{weight_casts::exact_float_weights, ReductionError}; +use crate::types::i64_to_exact_f64; + +impl_variant_reduction!( + BinPacking, + => , + fields: [num_items, max_numeric_magnitude_bits], + |src| BinPacking::new( + exact_float_weights::, BinPacking>(src.sizes())?, + i64_to_exact_f64(*src.capacity()) + .map_err(ReductionError::inexact_float_conversion::, BinPacking>)?, + ).map_err(ReductionError::construction::, BinPacking>)? +); + +crate::register_aggregate_reduction!( + crate::rules::VariantReductionResult, BinPacking> +); diff --git a/src/rules/binpacking_ilp.rs b/src/rules/binpacking_ilp.rs index 37657bc44..681957479 100644 --- a/src/rules/binpacking_ilp.rs +++ b/src/rules/binpacking_ilp.rs @@ -92,7 +92,7 @@ impl ReduceTo> for BinPacking { let objective: Vec<(usize, i64)> = (0..n).map(|j| (n * n + j, 1)).collect(); let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + .map_err(>>::target_construction)?; Ok(ReductionBPToILP { target, n }) } diff --git a/src/rules/comparativecontainment_casts.rs b/src/rules/comparativecontainment_casts.rs new file mode 100644 index 000000000..3c3bcefa3 --- /dev/null +++ b/src/rules/comparativecontainment_casts.rs @@ -0,0 +1,34 @@ +//! Numeric variant reductions for ComparativeContainment. + +use crate::impl_variant_reduction; +use crate::models::set::ComparativeContainment; +use crate::rules::{weight_casts::exact_float_weights, ReductionError}; +use crate::types::One; + +impl_variant_reduction!( + ComparativeContainment, + => , + fields: [universe_size, num_r_sets, num_s_sets], + |src| ComparativeContainment::new( + src.universe_size(), src.r_sets().to_vec(), src.s_sets().to_vec(), + ).map_err(ReductionError::construction::, ComparativeContainment>)? +); + +crate::register_aggregate_reduction!( + crate::rules::VariantReductionResult, ComparativeContainment> +); + +impl_variant_reduction!( + ComparativeContainment, + => , + fields: [universe_size, num_r_sets, num_s_sets], + |src| ComparativeContainment::with_weights( + src.universe_size(), src.r_sets().to_vec(), src.s_sets().to_vec(), + exact_float_weights::, ComparativeContainment>(src.r_weights())?, + exact_float_weights::, ComparativeContainment>(src.s_weights())?, + ).map_err(ReductionError::construction::, ComparativeContainment>)? +); + +crate::register_aggregate_reduction!( + crate::rules::VariantReductionResult, ComparativeContainment> +); diff --git a/src/rules/longestpath_casts.rs b/src/rules/longestpath_casts.rs new file mode 100644 index 000000000..e3a9d5e52 --- /dev/null +++ b/src/rules/longestpath_casts.rs @@ -0,0 +1,20 @@ +//! Numeric variant reductions for LongestPath. + +use crate::impl_variant_reduction; +use crate::models::graph::LongestPath; +use crate::topology::SimpleGraph; +use crate::types::One; + +impl_variant_reduction!( + LongestPath, + => , + fields: [num_vertices, num_edges], + |src| LongestPath::new(src.graph().clone(), vec![1_i64; src.num_edges()], src.source_vertex(), src.target_vertex()) +); + +crate::register_aggregate_reduction!( + crate::rules::VariantReductionResult< + LongestPath, + LongestPath, + > +); diff --git a/src/rules/maxcut_casts.rs b/src/rules/maxcut_casts.rs new file mode 100644 index 000000000..111f23260 --- /dev/null +++ b/src/rules/maxcut_casts.rs @@ -0,0 +1,17 @@ +//! Numeric variant reductions for MaxCut. + +use crate::impl_variant_reduction; +use crate::models::graph::MaxCut; +use crate::topology::SimpleGraph; +use crate::types::One; + +impl_variant_reduction!( + MaxCut, + => , + fields: [num_vertices, num_edges], + |src| MaxCut::new(src.graph().clone(), vec![1_i64; src.num_edges()]) +); + +crate::register_aggregate_reduction!( + crate::rules::VariantReductionResult, MaxCut> +); diff --git a/src/rules/maximumclique_casts.rs b/src/rules/maximumclique_casts.rs new file mode 100644 index 000000000..d2dffa95f --- /dev/null +++ b/src/rules/maximumclique_casts.rs @@ -0,0 +1,20 @@ +//! Numeric variant reductions for MaximumClique. + +use crate::impl_variant_reduction; +use crate::models::graph::MaximumClique; +use crate::topology::SimpleGraph; +use crate::types::One; + +impl_variant_reduction!( + MaximumClique, + => , + fields: [num_vertices, num_edges], + |src| MaximumClique::new(src.graph().clone(), vec![1_i64; src.num_vertices()]) +); + +crate::register_aggregate_reduction!( + crate::rules::VariantReductionResult< + MaximumClique, + MaximumClique, + > +); diff --git a/src/rules/maximumcokplex_casts.rs b/src/rules/maximumcokplex_casts.rs new file mode 100644 index 000000000..003f16f96 --- /dev/null +++ b/src/rules/maximumcokplex_casts.rs @@ -0,0 +1,21 @@ +//! Numeric variant reductions for MaximumCoKPlex. + +use crate::impl_variant_reduction; +use crate::models::graph::MaximumCoKPlex; +use crate::topology::SimpleGraph; +use crate::types::One; +use crate::variant::KN; + +impl_variant_reduction!( + MaximumCoKPlex, + => , + fields: [num_vertices, num_edges], + |src| MaximumCoKPlex::with_k(src.graph().clone(), vec![1_i64; src.num_vertices()], src.bound_k()) +); + +crate::register_aggregate_reduction!( + crate::rules::VariantReductionResult< + MaximumCoKPlex, + MaximumCoKPlex, + > +); diff --git a/src/rules/maximumcokplex_ilp.rs b/src/rules/maximumcokplex_ilp.rs index 2b1ff1161..1471c9014 100644 --- a/src/rules/maximumcokplex_ilp.rs +++ b/src/rules/maximumcokplex_ilp.rs @@ -135,7 +135,8 @@ impl ReduceTo> for MaximumCoKPlex { ILP, >("encoding a degree or co-k-plex bound") })?; - reduce_cokplex_to_ilp(self, constraints, objective).map_err(Self::target_construction) + reduce_cokplex_to_ilp(self, constraints, objective) + .map_err(>>::target_construction) } } diff --git a/src/rules/maximumedgeweightedkclique_casts.rs b/src/rules/maximumedgeweightedkclique_casts.rs new file mode 100644 index 000000000..000960eae --- /dev/null +++ b/src/rules/maximumedgeweightedkclique_casts.rs @@ -0,0 +1,16 @@ +//! Numeric variant reductions for MaximumEdgeWeightedKClique. + +use crate::impl_variant_reduction; +use crate::models::graph::MaximumEdgeWeightedKClique; +use crate::rules::{weight_casts::exact_float_weights, ReductionError}; + +impl_variant_reduction!( + MaximumEdgeWeightedKClique, + => , + fields: [num_vertices, num_edges], + |src| MaximumEdgeWeightedKClique::new( + src.graph().clone(), + exact_float_weights::, MaximumEdgeWeightedKClique>(src.edge_weights())?, + src.k(), + ).map_err(ReductionError::construction::, MaximumEdgeWeightedKClique>)? +); diff --git a/src/rules/maximumindependentset_casts.rs b/src/rules/maximumindependentset_casts.rs index 1e7ac32b1..886590e01 100644 --- a/src/rules/maximumindependentset_casts.rs +++ b/src/rules/maximumindependentset_casts.rs @@ -4,6 +4,8 @@ use crate::impl_variant_reduction; use crate::models::graph::MaximumIndependentSet; +use crate::models::Decision; +use crate::rules::{weight_casts::exact_float_weights, ReduceTo, ReductionResult}; use crate::topology::{Graph, KingsSubgraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph}; use crate::types::One; @@ -174,3 +176,27 @@ crate::register_aggregate_reduction!( MaximumIndependentSet, > ); + +impl_variant_reduction!( + Decision, + > => >, + fields: [num_vertices, num_edges], + |src| Decision::new( + ReduceTo::>::reduce_to(src.inner())?.target_problem().clone(), + *src.bound()) +); +type DecisionWeightCast = crate::rules::VariantReductionResult< + Decision>, + Decision>, +>; +crate::register_aggregate_reduction!(DecisionWeightCast); + +impl_variant_reduction!( + MaximumIndependentSet, + => , + fields: [num_vertices, num_edges], + |src| MaximumIndependentSet::new( + src.graph().clone(), + exact_float_weights::, MaximumIndependentSet>(src.weights())?, + ) +); diff --git a/src/rules/minimumdominatingset_casts.rs b/src/rules/minimumdominatingset_casts.rs new file mode 100644 index 000000000..71ee27394 --- /dev/null +++ b/src/rules/minimumdominatingset_casts.rs @@ -0,0 +1,36 @@ +//! Numeric variant reductions for MinimumDominatingSet. + +use crate::impl_variant_reduction; +use crate::models::graph::MinimumDominatingSet; +use crate::models::Decision; +use crate::rules::{ReduceTo, ReductionResult}; +use crate::topology::SimpleGraph; +use crate::types::One; + +impl_variant_reduction!( + MinimumDominatingSet, + => , + fields: [num_vertices, num_edges], + |src| MinimumDominatingSet::new(src.graph().clone(), vec![1_i64; src.num_vertices()]) +); + +crate::register_aggregate_reduction!( + crate::rules::VariantReductionResult< + MinimumDominatingSet, + MinimumDominatingSet, + > +); + +impl_variant_reduction!( + Decision, + > => >, + fields: [num_vertices, num_edges], + |src| Decision::new( + ReduceTo::>::reduce_to(src.inner())?.target_problem().clone(), + *src.bound()) +); +type DecisionWeightCast = crate::rules::VariantReductionResult< + Decision>, + Decision>, +>; +crate::register_aggregate_reduction!(DecisionWeightCast); diff --git a/src/rules/minimumfeedbackvertexset_casts.rs b/src/rules/minimumfeedbackvertexset_casts.rs new file mode 100644 index 000000000..514b07926 --- /dev/null +++ b/src/rules/minimumfeedbackvertexset_casts.rs @@ -0,0 +1,19 @@ +//! Numeric variant reductions for MinimumFeedbackVertexSet. + +use crate::impl_variant_reduction; +use crate::models::graph::MinimumFeedbackVertexSet; +use crate::types::One; + +impl_variant_reduction!( + MinimumFeedbackVertexSet, + => , + fields: [num_vertices, num_arcs], + |src| MinimumFeedbackVertexSet::new(src.graph().clone(), vec![1_i64; src.num_vertices()]) +); + +crate::register_aggregate_reduction!( + crate::rules::VariantReductionResult< + MinimumFeedbackVertexSet, + MinimumFeedbackVertexSet, + > +); diff --git a/src/rules/minimumtardinesssequencing_casts.rs b/src/rules/minimumtardinesssequencing_casts.rs new file mode 100644 index 000000000..8e21f56d4 --- /dev/null +++ b/src/rules/minimumtardinesssequencing_casts.rs @@ -0,0 +1,19 @@ +//! Numeric variant reductions for MinimumTardinessSequencing. + +use crate::impl_variant_reduction; +use crate::models::misc::MinimumTardinessSequencing; +use crate::types::One; + +impl_variant_reduction!( + MinimumTardinessSequencing, + => , + fields: [num_tasks, num_precedences, max_processing_time_bits], + |src| MinimumTardinessSequencing::with_lengths(vec![1_i64; src.num_tasks()], src.deadlines().to_vec(), src.precedences().to_vec()) +); + +crate::register_aggregate_reduction!( + crate::rules::VariantReductionResult< + MinimumTardinessSequencing, + MinimumTardinessSequencing, + > +); diff --git a/src/rules/minimumtardinesssequencing_ilp.rs b/src/rules/minimumtardinesssequencing_ilp.rs index 1955399c7..a8430c472 100644 --- a/src/rules/minimumtardinesssequencing_ilp.rs +++ b/src/rules/minimumtardinesssequencing_ilp.rs @@ -122,9 +122,17 @@ impl ReduceTo> for MinimumTardinessSequencing { let num_x_vars = n * n; let num_vars = num_x_vars + n; let positions = (0..n) - .map(|position| Self::exact_i64(position, "representing a task position in ILP rows")) + .map(|position| { + >>::exact_i64( + position, + "representing a task position in ILP rows", + ) + }) .collect::, _>>()?; - let big_m = Self::exact_i64(n, "representing the number of tasks in ILP rows")?; + let big_m = >>::exact_i64( + n, + "representing the number of tasks in ILP rows", + )?; let x_var = |j: usize, p: usize| -> usize { j * n + p }; let u_var = |j: usize| -> usize { num_x_vars + j }; @@ -145,7 +153,7 @@ impl ReduceTo> for MinimumTardinessSequencing { Ok(ReductionMTSToILP { target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?, + .map_err(>>::target_construction)?, num_tasks: n, }) } diff --git a/src/rules/minimumvertexcover_casts.rs b/src/rules/minimumvertexcover_casts.rs new file mode 100644 index 000000000..edfb7a254 --- /dev/null +++ b/src/rules/minimumvertexcover_casts.rs @@ -0,0 +1,41 @@ +//! Variant reductions for MinimumVertexCover. + +use crate::impl_variant_reduction; +use crate::models::graph::MinimumVertexCover; +use crate::models::Decision; +use crate::rules::{ReduceTo, ReductionResult}; +use crate::topology::SimpleGraph; +use crate::types::One; + +impl_variant_reduction!( + MinimumVertexCover, + => , + fields: [num_vertices, num_edges], + |src| MinimumVertexCover::new( + src.graph().clone(), vec![1_i64; src.num_vertices()]) +); + +crate::register_aggregate_reduction!( + crate::rules::VariantReductionResult< + MinimumVertexCover, + MinimumVertexCover, + > +); + +#[cfg(test)] +#[path = "../unit_tests/rules/minimumvertexcover_casts.rs"] +mod tests; + +impl_variant_reduction!( + Decision, + > => >, + fields: [num_vertices, num_edges], + |src| Decision::new( + ReduceTo::>::reduce_to(src.inner())?.target_problem().clone(), + *src.bound()) +); +type DecisionWeightCast = crate::rules::VariantReductionResult< + Decision>, + Decision>, +>; +crate::register_aggregate_reduction!(DecisionWeightCast); diff --git a/src/rules/minmaxmulticenter_casts.rs b/src/rules/minmaxmulticenter_casts.rs new file mode 100644 index 000000000..7d687bfd9 --- /dev/null +++ b/src/rules/minmaxmulticenter_casts.rs @@ -0,0 +1,20 @@ +//! Numeric variant reductions for MinMaxMulticenter. + +use crate::impl_variant_reduction; +use crate::models::graph::MinMaxMulticenter; +use crate::topology::SimpleGraph; +use crate::types::One; + +impl_variant_reduction!( + MinMaxMulticenter, + => , + fields: [num_vertices, num_edges, max_numeric_magnitude_bits], + |src| MinMaxMulticenter::new(src.graph().clone(), vec![1_i64; src.num_vertices()], vec![1_i64; src.num_edges()], src.k()) +); + +crate::register_aggregate_reduction!( + crate::rules::VariantReductionResult< + MinMaxMulticenter, + MinMaxMulticenter, + > +); diff --git a/src/rules/mixedchinesepostman_casts.rs b/src/rules/mixedchinesepostman_casts.rs new file mode 100644 index 000000000..4c5badf4b --- /dev/null +++ b/src/rules/mixedchinesepostman_casts.rs @@ -0,0 +1,16 @@ +//! Numeric variant reductions for MixedChinesePostman. + +use crate::impl_variant_reduction; +use crate::models::graph::MixedChinesePostman; +use crate::types::One; + +impl_variant_reduction!( + MixedChinesePostman, + => , + fields: [num_vertices, num_arcs, num_edges], + |src| MixedChinesePostman::new(src.graph().clone(), vec![1_i64; src.num_arcs()], vec![1_i64; src.num_edges()]) +); + +crate::register_aggregate_reduction!( + crate::rules::VariantReductionResult, MixedChinesePostman> +); diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 3baa44186..62a3d5929 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -2,6 +2,11 @@ pub mod analysis; pub mod registry; +mod weight_casts; + +#[cfg(test)] +#[path = "../unit_tests/rules/weight_casts.rs"] +mod test_weight_casts; pub use registry::{ EdgeCapabilities, ParameterContractError, ReductionEntry, ReductionParameterContract, ReductionParameterDeclarations, UnavailableParameterField, @@ -36,6 +41,8 @@ pub(crate) mod minimumweightandorgraph_ilp; pub(crate) mod partitionintoperfectmatchings_ilp; pub(crate) mod twodimensionalconsecutivesets_ilp; pub(crate) use graph::{recover_completed_result, RecoveryStep}; +mod binpacking_casts; +mod comparativecontainment_casts; pub(crate) mod graph_helpers; pub(crate) mod graphpartitioning_maxcut; pub(crate) mod graphpartitioning_qubo; @@ -84,10 +91,15 @@ pub(crate) mod ksatisfiability_simultaneousincongruences; pub(crate) mod ksatisfiability_subsetsum; pub(crate) mod ksatisfiability_timetabledesign; pub(crate) mod longestcommonsubsequence_maximumindependentset; +mod longestpath_casts; +mod maxcut_casts; pub(crate) mod maxcut_minimumcutintoboundedsets; pub(crate) mod maxcut_minimummatrixcover; pub(crate) mod maximum2satisfiability_maxcut; +mod maximumclique_casts; pub(crate) mod maximumclique_maximumindependentset; +mod maximumcokplex_casts; +mod maximumedgeweightedkclique_casts; mod maximumindependentset_casts; mod maximumindependentset_gridgraph; pub(crate) mod maximumindependentset_maximumclique; @@ -100,10 +112,14 @@ pub(crate) mod minimumcodegenerationunlimitedregisters_ilp; pub(crate) mod minimumcostmaximumflow_minimumcostcirculation; pub(crate) mod minimumcoveringbycliques_minimumintersectiongraphbasis; pub(crate) mod minimumdiscreteplanarinversekinematics_qubo; +mod minimumdominatingset_casts; +mod minimumfeedbackvertexset_casts; pub(crate) mod minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters; pub(crate) mod minimummaximalmatching_maximumachromaticnumber; pub(crate) mod minimummaximalmatching_minimummatrixdomination; pub(crate) mod minimummultiwaycut_qubo; +mod minimumtardinesssequencing_casts; +mod minimumvertexcover_casts; pub(crate) mod minimumvertexcover_comparativecontainment; pub(crate) mod minimumvertexcover_ensemblecomputation; pub(crate) mod minimumvertexcover_longestcommonsubsequence; @@ -113,6 +129,8 @@ pub(crate) mod minimumvertexcover_minimumfeedbackvertexset; pub(crate) mod minimumvertexcover_minimumhittingset; pub(crate) mod minimumvertexcover_minimumsetcovering; pub(crate) mod minimumvertexcover_minimumweightandorgraph; +mod minmaxmulticenter_casts; +mod mixedchinesepostman_casts; pub(crate) mod naesatisfiability_maxcut; pub(crate) mod naesatisfiability_partitionintoperfectmatchings; pub(crate) mod naesatisfiability_setsplitting; @@ -132,6 +150,7 @@ pub(crate) mod partition_subsetsum; pub(crate) mod partition_sumofsquarespartition; pub(crate) mod partitionintocliques_minimumcoveringbycliques; pub(crate) mod partitionintopathsoflength2_boundedcomponentspanningforest; +mod prizecollectingsteinerforest_casts; pub(crate) mod prizecollectingsteinerforest_steinertree; mod qubo_casts; pub(crate) mod rootedtreearrangement_rootedtreestorageassignment; @@ -149,6 +168,7 @@ pub(crate) mod setsplitting_betweenness; mod spinglass_casts; pub(crate) mod spinglass_maxcut; pub(crate) mod spinglass_qubo; +mod steinertree_casts; pub(crate) mod subsetsum_closestvectorproblem; pub(crate) mod subsetsum_integerexpressionmembership; pub(crate) mod subsetsum_partition; diff --git a/src/rules/prizecollectingsteinerforest_casts.rs b/src/rules/prizecollectingsteinerforest_casts.rs new file mode 100644 index 000000000..d90c7d6ab --- /dev/null +++ b/src/rules/prizecollectingsteinerforest_casts.rs @@ -0,0 +1,32 @@ +//! Numeric variant reductions for PrizeCollectingSteinerForest. + +use crate::impl_variant_reduction; +use crate::models::graph::PrizeCollectingSteinerForest; +use crate::rules::{weight_casts::exact_float_weights, ReductionError}; +use crate::topology::SimpleGraph; +use crate::types::i64_to_exact_f64; + +type Source = PrizeCollectingSteinerForest; +type Target = PrizeCollectingSteinerForest; + +impl_variant_reduction!( + PrizeCollectingSteinerForest, + => , + fields: [num_vertices, num_edges, num_vertices_with_prize], + |src| { + let prizes = exact_float_weights::(src.vertex_prizes())?; + let costs = exact_float_weights::(src.edge_costs())?; + let convert = |value| i64_to_exact_f64(value).map_err(ReductionError::inexact_float_conversion::); + let beta = convert(*src.beta())?; + let omega = convert(*src.omega())?; + // All terms are nonnegative; this bounds every intermediate and complete objective. + let bound = src.vertex_prizes().iter().try_fold(0_i64, |sum, &v| sum.checked_add(v)) + .and_then(|sum| sum.checked_mul(*src.beta())) + .and_then(|sum| src.edge_costs().iter().try_fold(sum, |total, &v| total.checked_add(v))) + .and_then(|sum| i64::try_from(src.num_vertices()).ok()?.checked_mul(*src.omega())?.checked_add(sum)) + .ok_or_else(|| ReductionError::integer_overflow::("bounding floating-point forest objectives"))?; + convert(bound)?; + PrizeCollectingSteinerForest::new(src.graph().clone(), prizes, costs, beta, omega) + .map_err(ReductionError::construction::)? + } +); diff --git a/src/rules/steinertree_casts.rs b/src/rules/steinertree_casts.rs new file mode 100644 index 000000000..cdcca4953 --- /dev/null +++ b/src/rules/steinertree_casts.rs @@ -0,0 +1,20 @@ +//! Numeric variant reductions for SteinerTree. + +use crate::impl_variant_reduction; +use crate::models::graph::SteinerTree; +use crate::topology::SimpleGraph; +use crate::types::One; + +impl_variant_reduction!( + SteinerTree, + => , + fields: [num_vertices, num_edges, num_terminals], + |src| SteinerTree::new(src.graph().clone(), vec![1_i64; src.num_edges()], src.terminals().to_vec()) +); + +crate::register_aggregate_reduction!( + crate::rules::VariantReductionResult< + SteinerTree, + SteinerTree, + > +); diff --git a/src/rules/weight_casts.rs b/src/rules/weight_casts.rs new file mode 100644 index 000000000..ef1018225 --- /dev/null +++ b/src/rules/weight_casts.rs @@ -0,0 +1,23 @@ +//! Exact integer-to-float boundaries shared by numeric variant reductions. + +use crate::rules::ReductionError; +use crate::traits::Problem; +use crate::types::i64_to_exact_f64; + +pub(super) fn exact_float_weights( + weights: &[i64], +) -> Result, ReductionError> { + let convert = + |value| i64_to_exact_f64(value).map_err(ReductionError::inexact_float_conversion::); + let mut magnitude = 0_i64; + weights + .iter() + .map(|&value| { + let converted = convert(value)?; + // Bound all subset sums; both addends are below 2^53, so this i64 sum cannot overflow. + magnitude += value.abs(); + convert(magnitude)?; + Ok(converted) + }) + .collect() +} diff --git a/src/unit_tests/rules/minimumvertexcover_casts.rs b/src/unit_tests/rules/minimumvertexcover_casts.rs new file mode 100644 index 000000000..d8a3bb02c --- /dev/null +++ b/src/unit_tests/rules/minimumvertexcover_casts.rs @@ -0,0 +1,62 @@ +use crate::models::graph::MinimumVertexCover; +use crate::rules::ReductionGraph; +use crate::solvers::BruteForce; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::Problem; +use crate::types::{Min, One}; + +#[test] +fn test_minimumvertexcover_one_to_i64_closed_loop() { + type Source = MinimumVertexCover; + type Target = MinimumVertexCover; + let graph = ReductionGraph::new(); + let paths = graph.find_paths_up_to( + Source::NAME, + &ReductionGraph::variant_to_map(&Source::variant()), + Target::NAME, + &ReductionGraph::variant_to_map(&Target::variant()), + 1, + ); + assert_eq!(paths.len(), 1); + assert_eq!(paths[0].steps.len(), 2, "weight conversion must be direct"); + + for (input, optimum) in [ + (SimpleGraph::new(0, vec![]), 0), + (SimpleGraph::new(3, vec![]), 0), + (SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2)]), 2), + ] { + let source = Source::new(input.clone(), vec![One; input.num_vertices()]); + let chain = graph + .reduce_along_path(&paths[0], &source) + .unwrap() + .unwrap(); + let target = chain.target_problem::(); + assert_eq!(target.graph(), source.graph()); + assert_eq!(target.weights(), &vec![1_i64; input.num_vertices()]); + assert_eq!(target.parameters(), source.parameters()); + + for mask in 0..(1 << input.num_vertices()) { + let solution: Vec = (0..input.num_vertices()) + .map(|vertex| mask & (1 << vertex) != 0) + .collect(); + assert_eq!(target.evaluate(&solution), source.evaluate(&solution)); + assert_eq!( + chain.extract_solution::, _>(&solution).unwrap(), + solution + ); + } + let solution = BruteForce::new().solve(target).unwrap().unwrap(); + let recovered = chain.extract_solution::, _>(&solution).unwrap(); + assert_eq!(source.evaluate(&recovered).unwrap(), Min(Some(optimum))); + assert!(chain.has_value_mapping()); + assert_eq!( + chain + .extract_value(serde_json::to_value(target.evaluate(&solution).unwrap()).unwrap()) + .unwrap(), + serde_json::to_value(Min(Some(optimum))).unwrap(), + ); + assert!(chain + .extract_solution::, _>(&vec![false; input.num_vertices() + 1]) + .is_err()); + } +} diff --git a/src/unit_tests/rules/weight_casts.rs b/src/unit_tests/rules/weight_casts.rs new file mode 100644 index 000000000..353a61faf --- /dev/null +++ b/src/unit_tests/rules/weight_casts.rs @@ -0,0 +1,357 @@ +use crate::models::graph::*; +use crate::models::misc::{BinPacking, MinimumTardinessSequencing}; +use crate::models::set::ComparativeContainment; +use crate::models::Decision; +use crate::rules::{ReductionError, ReductionGraph}; +use crate::solvers::{BruteForce, BruteForceProblem}; +use crate::topology::{DirectedGraph, MixedGraph, SimpleGraph}; +use crate::traits::Problem; +use crate::types::{One, MAX_EXACT_F64_INTEGER}; +use crate::variant::KN; +use serde::Serialize; +use std::collections::BTreeSet; +use std::fmt::Debug; + +#[test] +fn registered_numeric_variants_have_direct_promotions() { + let variants: BTreeSet<_> = crate::registry::variant_entries() + .into_iter() + .map(|v| (v.name, v.variant_map())) + .collect(); + let edges: BTreeSet<_> = crate::rules::registry::reduction_entries() + .into_iter() + .filter(|e| e.source_name == e.target_name && e.reduce_fn.is_some()) + .map(|e| { + ( + e.source_name, + ReductionGraph::variant_to_map(&e.source_variant()), + ReductionGraph::variant_to_map(&e.target_variant()), + ) + }) + .collect(); + let mut missing = Vec::new(); + for (name, source) in &variants { + for field in ["weight", "coefficient"] { + let to = match source.get(field).map(String::as_str) { + Some("One") => "i64", + Some("i64") => "f64", + _ => continue, + }; + let mut target = source.clone(); + target.insert(field.into(), to.into()); + if variants.contains(&(*name, target.clone())) + && !edges.contains(&(*name, source.clone(), target.clone())) + { + missing.push(format!("{name} {source:?} -> {target:?}")); + } + } + } + assert!( + missing.is_empty(), + "missing numeric promotions:\n{}", + missing.join("\n") + ); +} + +fn assert_cast(source: S, maps_values: bool) +where + S: Problem + BruteForceProblem + Serialize + 'static, + T: Problem + + BruteForceProblem + + Serialize + + serde::de::DeserializeOwned + + 'static, + S::Solution: Eq + std::hash::Hash + Debug, + S::Value: PartialEq + Debug + Serialize + crate::types::SolutionAggregate, + T::Value: Serialize + crate::types::SolutionAggregate, +{ + let graph = ReductionGraph::new(); + let paths = graph.find_paths_up_to( + S::NAME, + &ReductionGraph::variant_to_map(&S::variant()), + T::NAME, + &ReductionGraph::variant_to_map(&T::variant()), + 1, + ); + assert_eq!( + paths.len(), + 1, + "{} {:?} -> {:?}", + S::NAME, + S::variant(), + T::variant() + ); + assert_eq!( + paths[0].steps.len(), + 2, + "{} promotion must be direct", + S::NAME + ); + let chain = graph + .reduce_along_path(&paths[0], &source) + .unwrap() + .unwrap(); + let target = chain.target_problem::(); + assert_eq!(target.parameters(), source.parameters()); + let expected: T = serde_json::from_value(serde_json::to_value(&source).unwrap()).unwrap(); + assert_eq!( + serde_json::to_value(target).unwrap(), + serde_json::to_value(expected).unwrap() + ); + let solver = BruteForce::new(); + let source_solution = solver.solve(&source).unwrap(); + let target_solutions = solver.find_all_witnesses(target).unwrap(); + if let Some(source_solution) = source_solution { + let optimum = source.evaluate(&source_solution).unwrap(); + assert!(!target_solutions.is_empty()); + for solution in target_solutions { + let recovered = chain.extract_solution::(&solution).unwrap(); + assert_eq!(recovered, solution); + let recovered_value = source.evaluate(&recovered).unwrap(); + assert_eq!(recovered_value, optimum); + if maps_values { + assert!(chain.has_value_mapping()); + let value = serde_json::to_value(target.evaluate(&solution).unwrap()).unwrap(); + assert_eq!( + chain.extract_value(value).unwrap(), + serde_json::to_value(recovered_value).unwrap() + ); + } + } + } else { + assert!(target_solutions.is_empty()); + } +} + +#[test] +fn unit_weight_promotions_preserve_instances_and_optima() { + let g = SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2)]); + assert_cast::, MaxCut>( + MaxCut::new(g.clone(), vec![One; 3]), + true, + ); + assert_cast::, MaximumClique>( + MaximumClique::new(g.clone(), vec![One; 4]), + true, + ); + assert_cast::, MaximumCoKPlex>( + MaximumCoKPlex::with_k(g.clone(), vec![One; 4], 2), + true, + ); + assert_cast::, MinimumDominatingSet>( + MinimumDominatingSet::new(g.clone(), vec![One; 4]), + true, + ); + assert_cast::, MinimumFeedbackVertexSet>( + MinimumFeedbackVertexSet::new( + DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]), + vec![One; 3], + ), + true, + ); + assert_cast::, LongestPath>( + LongestPath::new(g.clone(), vec![One; 3], 0, 2), + true, + ); + assert_cast::, MinMaxMulticenter>( + MinMaxMulticenter::new(g.clone(), vec![One; 4], vec![One; 3], 2), + true, + ); + assert_cast::, SteinerTree>( + SteinerTree::unit_weights(g, vec![0, 2]), + true, + ); + assert_cast::, MixedChinesePostman>( + MixedChinesePostman::new( + MixedGraph::new(2, vec![(0, 1)], vec![(0, 1)]), + vec![One], + vec![One], + ), + true, + ); + assert_cast::, MinimumTardinessSequencing>( + MinimumTardinessSequencing::new(3, vec![1, 1, 2], vec![(0, 2)]), + true, + ); + assert_cast::, ComparativeContainment>( + ComparativeContainment::new(2, vec![vec![0]], vec![vec![1], vec![0, 1]]).unwrap(), + true, + ); +} + +#[test] +fn decision_weight_promotions_preserve_bounds_and_answers() { + let g = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + for bound in [-1, 1, 2, i64::MAX] { + assert_cast::< + Decision>, + Decision>, + >( + Decision::new(MaximumIndependentSet::new(g.clone(), vec![One; 3]), bound), + true, + ); + assert_cast::< + Decision>, + Decision>, + >( + Decision::new(MinimumDominatingSet::new(g.clone(), vec![One; 3]), bound), + true, + ); + assert_cast::< + Decision>, + Decision>, + >( + Decision::new(MinimumVertexCover::new(g.clone(), vec![One; 3]), bound), + true, + ); + } +} + +#[test] +fn integer_weight_promotions_preserve_optima_and_feasibility() { + let g = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + assert_cast::, MaximumIndependentSet>( + MaximumIndependentSet::new(g.clone(), vec![3, -2, 5]), + false, + ); + assert_cast::, MaximumEdgeWeightedKClique>( + MaximumEdgeWeightedKClique::new(g.clone(), vec![-2, 3], 2).unwrap(), + false, + ); + assert_cast::, ComparativeContainment>( + ComparativeContainment::with_weights( + 2, + vec![vec![0], vec![0, 1]], + vec![vec![1], vec![0, 1]], + vec![2, 5], + vec![3, 6], + ) + .unwrap(), + true, + ); + assert_cast::, BinPacking>( + BinPacking::new(vec![2, 3, 4], 5).unwrap(), + true, + ); + assert_cast::< + PrizeCollectingSteinerForest<_, i64>, + PrizeCollectingSteinerForest, + >( + PrizeCollectingSteinerForest::new(g, vec![5, 2, 5], vec![1, 6], 1, 2).unwrap(), + false, + ); +} + +fn assert_float_rejected(source: S) { + let graph = ReductionGraph::new(); + let paths = graph.find_paths_up_to( + S::NAME, + &ReductionGraph::variant_to_map(&S::variant()), + T::NAME, + &ReductionGraph::variant_to_map(&T::variant()), + 1, + ); + assert_eq!(paths.len(), 1); + assert_eq!(paths[0].steps.len(), 2); + assert!(matches!( + graph.reduce_along_path(&paths[0], &source), + Err(ReductionError::InexactFloatConversion { .. }) + | Err(ReductionError::IntegerOverflow { .. }) + )); +} + +#[test] +fn float_promotions_reject_inexact_accumulations_and_products() { + let big = MAX_EXACT_F64_INTEGER; + assert_float_rejected::, MaximumIndependentSet>( + MaximumIndependentSet::new(SimpleGraph::new(3, vec![]), vec![big, 1, 1]), + ); + assert_float_rejected::, MaximumEdgeWeightedKClique>( + MaximumEdgeWeightedKClique::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + vec![big, 1, 1], + 3, + ) + .unwrap(), + ); + assert_float_rejected::, ComparativeContainment>( + ComparativeContainment::with_weights( + 1, + vec![vec![0]; 3], + vec![vec![0]], + vec![big, 1, 1], + vec![big], + ) + .unwrap(), + ); + assert_float_rejected::, BinPacking>( + BinPacking::new(vec![big, 1, 1], big).unwrap(), + ); + assert_float_rejected::< + PrizeCollectingSteinerForest<_, i64>, + PrizeCollectingSteinerForest, + >( + PrizeCollectingSteinerForest::new(SimpleGraph::new(1, vec![]), vec![big], vec![], 2, 0) + .unwrap(), + ); +} + +#[test] +fn float_promotions_reject_unrepresentable_input_fields() { + let inexact = MAX_EXACT_F64_INTEGER + 2; + for weight in [inexact, i64::MIN] { + assert_float_rejected::< + MaximumIndependentSet<_, i64>, + MaximumIndependentSet, + >(MaximumIndependentSet::new( + SimpleGraph::new(1, vec![]), + vec![weight], + )); + assert_float_rejected::, MaximumEdgeWeightedKClique>( + MaximumEdgeWeightedKClique::new(SimpleGraph::new(2, vec![(0, 1)]), vec![weight], 2) + .unwrap(), + ); + assert_float_rejected::, BinPacking>( + BinPacking::new(vec![1], weight).unwrap(), + ); + } + assert_float_rejected::, ComparativeContainment>( + ComparativeContainment::with_weights( + 1, + vec![vec![0]], + vec![vec![0]], + vec![1], + vec![inexact], + ) + .unwrap(), + ); + assert_float_rejected::< + PrizeCollectingSteinerForest<_, i64>, + PrizeCollectingSteinerForest, + >( + PrizeCollectingSteinerForest::new(SimpleGraph::new(1, vec![]), vec![1], vec![], inexact, 0) + .unwrap(), + ); + assert_float_rejected::, BinPacking>( + BinPacking::new(vec![inexact], 1).unwrap(), + ); + assert_float_rejected::< + PrizeCollectingSteinerForest<_, i64>, + PrizeCollectingSteinerForest, + >( + PrizeCollectingSteinerForest::new(SimpleGraph::new(1, vec![]), vec![inexact], vec![], 1, 0) + .unwrap(), + ); +} + +#[test] +fn float_forest_promotion_rejects_overflowing_scaled_costs() { + let big = MAX_EXACT_F64_INTEGER; + assert_float_rejected::< + PrizeCollectingSteinerForest<_, i64>, + PrizeCollectingSteinerForest, + >( + PrizeCollectingSteinerForest::new(SimpleGraph::new(1, vec![]), vec![big], vec![], big, 0) + .unwrap(), + ); +}