From e6653caa7aebc51145f68433db841845a7010cdc Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:00:22 -0800 Subject: [PATCH] Add dominating set to set covering reduction --- docs/paper/reductions.typ | 126 ++++++++ docs/paper/references.bib | 8 + problemreductions-cli/tests/cli_tests.rs | 5 +- src/models/set/minimum_set_covering.rs | 2 +- src/rules/minimumdominatingset_ilp.rs | 2 +- ...minimumdominatingset_minimumsetcovering.rs | 87 +++++ src/rules/mod.rs | 2 + ...minimumdominatingset_minimumsetcovering.rs | 305 ++++++++++++++++++ 8 files changed, 534 insertions(+), 3 deletions(-) create mode 100644 src/rules/minimumdominatingset_minimumsetcovering.rs create mode 100644 src/unit_tests/rules/minimumdominatingset_minimumsetcovering.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 8e0462b7a..dc4f0128c 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -11801,6 +11801,132 @@ the displayed rule, extracted from the corresponding `pred path` entry. _Solution extraction._ For IS $I subset.eq V'$, return packing $cal(P) = {S_i : v_i in I}$ (same variable assignment). ] +// MinimumDominatingSet -> MinimumSetCovering +#let mds-sc = load-example("MinimumDominatingSet", "MinimumSetCovering") +#let mds-sc-sol = mds-sc.solutions.at(0) +#let mds-sc-source = mds-sc.source.instance +#let mds-sc-target = mds-sc.target.instance +#let mds-sc-n = graph-num-vertices(mds-sc-source) +#assert(mds-sc-n >= 3 and mds-sc-source.graph.edges == range(mds-sc-n - 1).map(v => (v, v + 1))) +#let mds-sc-indices = config => config.enumerate().filter(((v, selected)) => selected).map(((v, _)) => v) +#let mds-sc-cost = (config, weights) => mds-sc-indices(config).map(v => weights.at(v)).fold(0, (total, w) => total + w) +#let mds-sc-covered = config => range(mds-sc-target.universe_size).filter(u => + mds-sc-target.sets.enumerate().any(((v, subset)) => config.at(v) and u in subset)) +#let mds-sc-dominates = config => range(mds-sc-n).all(u => config.at(u) or + mds-sc-source.graph.edges.any(e => (e.at(0) == u and config.at(e.at(1))) or + (e.at(1) == u and config.at(e.at(0))))) +#let mds-sc-configs = range(calc.pow(2, mds-sc-n)).map(mask => range(mds-sc-n).map(v => + calc.rem(calc.floor(mask / calc.pow(2, v)), 2) == 1)) +#let mds-sc-optimum = mds-sc-cost(mds-sc-sol.target_config, mds-sc-target.weights) +#let mds-sc-bound = mds-sc-optimum - 1 +#let mds-sc-no-candidates = mds-sc-configs.filter(config => mds-sc-cost(config, mds-sc-target.weights) <= mds-sc-bound) +#let mds-sc-alternatives = mds-sc-configs.filter(config => + mds-sc-covered(config).len() == mds-sc-target.universe_size and + mds-sc-cost(config, mds-sc-target.weights) > mds-sc-optimum +).sorted(key: config => mds-sc-cost(config, mds-sc-target.weights)).slice(0, 2) +#assert(mds-sc-target.universe_size == mds-sc-n and mds-sc-target.sets.len() == mds-sc-n) +#assert(mds-sc-target.weights == mds-sc-source.weights) +#assert(mds-sc-target.sets.map(s => s.len()).fold(0, (total, size) => total + size) == + mds-sc-n + 2 * graph-num-edges(mds-sc-source)) +#assert(mds-sc-sol.source_config == mds-sc-sol.target_config and mds-sc-dominates(mds-sc-sol.source_config)) +#assert(mds-sc-covered(mds-sc-sol.target_config).len() == mds-sc-target.universe_size) +#assert(mds-sc-cost(mds-sc-sol.source_config, mds-sc-source.weights) == mds-sc-optimum) +#assert(mds-sc-configs.filter(config => mds-sc-covered(config).len() == mds-sc-target.universe_size and + mds-sc-cost(config, mds-sc-target.weights) <= mds-sc-optimum) == (mds-sc-sol.target_config,)) +#assert(mds-sc-target.weights.all(w => w > 0) and mds-sc-no-candidates.len() > 0) +#assert(mds-sc-no-candidates.all(config => not mds-sc-dominates(config) and + mds-sc-covered(config).len() < mds-sc-target.universe_size)) +#assert(mds-sc-alternatives.len() == 2 and mds-sc-alternatives.all(config => mds-sc-dominates(config))) +#reduction-rule("MinimumDominatingSet", "MinimumSetCovering", + example: true, + example-caption: [Closed neighbourhoods of a weighted path], + extra: [ + #pred-commands( + "pred create --example " + rule-spec(mds-sc) + " -o dominating.json", + "pred reduce dominating.json --via route.json -o bundle.json", + "pred solve bundle.json", + "pred evaluate dominating.json --config " + cli-config(mds-sc-sol.source_config), + ) + *Source and construction.* The source has #mds-sc-n vertices, edges + #{mds-sc-source.graph.edges.map(e => $(#e.at(0), #e.at(1))$).join(", ")}, and + weights $(#fmt-values(mds-sc-source.weights))$. The universe has + #mds-sc-target.universe_size elements and the indexed family has + #mds-sc-target.sets.len() sets: + #table( + columns: (auto, 1fr, auto), + [Index], [Closed neighbourhood], [Weight], + ..mds-sc-target.sets.enumerate().map(((v, subset)) => ( + [#v], [${#fmt-values(subset)}$], [#mds-sc-target.weights.at(v)], + )).flatten(), + ) + The total membership count is + #mds-sc-target.sets.map(s => s.len()).fold(0, (total, size) => total + size), + equal to $#mds-sc-n + 2 dot #graph-num-edges(mds-sc-source)$. + + *YES and extraction.* Selecting sets with indices + ${#fmt-values(mds-sc-indices(mds-sc-sol.target_config))}$ covers + ${#fmt-values(mds-sc-covered(mds-sc-sol.target_config))}$ at cost + #mds-sc-optimum. The identical source vector selects vertices + ${#fmt-values(mds-sc-indices(mds-sc-sol.source_config))}$ and dominates the graph + with cost #mds-sc-cost(mds-sc-sol.source_config, mds-sc-source.weights). + Enumeration of the fixture's configurations confirms this is the unique optimum. + The following feasible choices are suboptimal: + #for config in mds-sc-alternatives [ + indices ${#fmt-values(mds-sc-indices(config))}$ cost + #mds-sc-cost(config, mds-sc-target.weights) on each side; + ] + + *NO at a bound.* For this same graph and target family, impose bound + #mds-sc-bound. All weights are positive. The only choices of cost at most + #mds-sc-bound are the #mds-sc-no-candidates.len() choices below: + #for config in mds-sc-no-candidates [ + indices ${#fmt-values(mds-sc-indices(config))}$ have cost + #mds-sc-cost(config, mds-sc-target.weights), cover + ${#fmt-values(mds-sc-covered(config))}$, and leave + ${#fmt-values(range(mds-sc-target.universe_size).filter(u => u not in mds-sc-covered(config)))}$ + uncovered and undominated; + ] + Thus neither side admits a witness within the bound. Without the bound, + both remain feasible with optimum #mds-sc-optimum. + ], +)[ + The closed-neighbourhood construction @umass2018dominatingcover turns weighted + domination into weighted covering with exactly $|V|$ universe elements and + $|V|$ indexed sets. Garey--Johnson @garey1979 gives the underlying problem + definitions; copying weights extends the construction to signed costs. +][ + _Construction._ Let $G=(V,E)$ be a simple undirected graph with + $V={0,...,n-1}$ and weights $w_v$ in `i64`. Set $U=V$ and, for each vertex + $v$, create $D_v={v} union {u : {u,v} in E}$ with weight $w_v$. + Keep every indexed set even when neighbourhoods coincide. Each member is in + $U$, and one weight accompanies each of the $n$ sets. Each edge contributes + two incidences, so total membership is $n+2|E|$ and storage is $O(n+|E|)$; + sorting the sets takes polynomial time. + + _Correctness._ ($arrow.r.double$) Given a dominating vertex selection $S$, + every $u in V$ either lies in $S$, hence in $D_u$, or has a selected neighbour + $v$, hence lies in $D_v$. The corresponding indexed sets cover $U$. + ($arrow.l.double$) Given a cover, every vertex $u$ belongs to some selected + $D_v$. Then either $u=v$ or ${u,v} in E$, so the same selected indices dominate + every source vertex. Costs agree term by term, giving a bijection of feasible + configurations and preserving the optimum and every optimal witness. + + _Solution extraction._ Validate the target witness once and copy its Boolean + vector: set index $v$ becomes vertex index $v$. Completed minimum values map + identically. Infeasible and wrong-length witnesses are rejected; evaluation + errors propagate. Both models add selected weights in increasing index order, + so `i64` overflow occurs at the same step, including before later cancellation. + Construction itself only copies weights; no objective arithmetic is needed. + + _Special cases._ The empty graph gives an empty universe and family with + cost zero. An isolated vertex gives a forced singleton, whatever its sign. + Disconnected graphs need no extra condition; zero or negative weights may + make redundant selections optimal. Equal neighbourhoods retain separate + indices and costs. Self-loops and repeated edges are rejected by the source + graph type. Every unbounded instance here is feasible by selecting all + vertices or sets, so a NO example necessarily specifies a cost bound. +] + #reduction-rule("MinimumVertexCover", "MinimumSetCovering")[ A vertex cover must "hit" every edge; set covering must "hit" every universe element. By making each edge a universe element and each vertex the set of its incident edges, the two covering conditions become identical. This is the canonical embedding of vertex cover as a special case of set covering. ][ diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 0dda81fd2..c34091905 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -292,6 +292,14 @@ @book{garey1979 year = {1979} } +@misc{umass2018dominatingcover, + author = {{University of Massachusetts Amherst, COMPSCI 311}}, + title = {Second Midterm Exam Solutions, Problem 8(a)}, + year = {2018}, + month = nov, + url = {https://people.cs.umass.edu/~marius/class/cs311-fa18/midterm2-sol.pdf} +} + @article{orlin1977, author = {James B. Orlin}, title = {Contentment in Graph Theory: Covering Graphs with Cliques}, diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 43aa0d8ef..e8b6563ec 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -6884,7 +6884,10 @@ fn test_inspect_excludes_non_witness_reductions() { String::from_utf8_lossy(&inspect.stderr) ); let json: serde_json::Value = serde_json::from_slice(&inspect.stdout).unwrap(); - assert_eq!(json["reduces_to"], serde_json::json!(["ILP"])); + assert_eq!( + json["reduces_to"], + serde_json::json!(["ILP", "MinimumSetCovering"]) + ); std::fs::remove_file(problem_file).unwrap(); } diff --git a/src/models/set/minimum_set_covering.rs b/src/models/set/minimum_set_covering.rs index 293434781..a7bd7717d 100644 --- a/src/models/set/minimum_set_covering.rs +++ b/src/models/set/minimum_set_covering.rs @@ -119,7 +119,7 @@ impl MinimumSetCovering { .unwrap_or_else(|error| panic!("{error}")) } - fn try_with_weights( + pub(crate) fn try_with_weights( universe_size: usize, sets: Vec>, weights: Vec, diff --git a/src/rules/minimumdominatingset_ilp.rs b/src/rules/minimumdominatingset_ilp.rs index 3478301d5..089f12736 100644 --- a/src/rules/minimumdominatingset_ilp.rs +++ b/src/rules/minimumdominatingset_ilp.rs @@ -86,7 +86,7 @@ impl ReduceTo> for MinimumDominatingSet { .collect(); let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + .map_err(>>::target_construction)?; Ok(ReductionDSToILP { target }) } diff --git a/src/rules/minimumdominatingset_minimumsetcovering.rs b/src/rules/minimumdominatingset_minimumsetcovering.rs new file mode 100644 index 000000000..6babe4b6d --- /dev/null +++ b/src/rules/minimumdominatingset_minimumsetcovering.rs @@ -0,0 +1,87 @@ +//! Closed-neighborhood reduction from MinimumDominatingSet to MinimumSetCovering. + +use crate::models::graph::MinimumDominatingSet; +use crate::models::set::MinimumSetCovering; +use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::topology::SimpleGraph; + +/// The target's set indices are the source's vertex indices. +#[derive(Debug, Clone)] +pub struct ReductionDSToSC { + target: MinimumSetCovering, +} + +impl ReductionResult for ReductionDSToSC { + type Source = MinimumDominatingSet; + type Target = MinimumSetCovering; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_solution( + &self, + target_solution: &::Solution, + ) -> crate::rules::ExtractionResult<::Solution> { + crate::rules::traits::validate_target_witness( + &self.target, + target_solution, + |value| value.0.is_some(), + "selected sets do not cover the universe", + )?; + Ok(target_solution.to_vec()) + } +} + +#[crate::aggregate_reduction(identity)] +impl crate::rules::AggregateReductionResult for ReductionDSToSC {} + +#[crate::reduction(transform = exact { + universe_size = "num_vertices", + num_sets = "num_vertices", +})] +impl ReduceTo> for MinimumDominatingSet { + type Result = ReductionDSToSC; + + fn reduce_to(&self) -> Result { + let sets = (0..self.num_vertices()) + .map(|v| { + let mut neighborhood: Vec<_> = self.closed_neighborhood(v).into_iter().collect(); + neighborhood.sort_unstable(); + neighborhood + }) + .collect(); + let target = MinimumSetCovering::try_with_weights( + self.num_vertices(), + sets, + self.weights().to_vec(), + ) + .map_err(>>::target_construction)?; + Ok(ReductionDSToSC { target }) + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "minimumdominatingset_to_minimumsetcovering", + build: || { + let source = MinimumDominatingSet::new( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + vec![3, 1, 4, 1, 3], + ); + let config = serde_json::json!([false, true, false, true, false]); + crate::example_db::specs::rule_example_with_witness::<_, MinimumSetCovering>( + source, + crate::export::SolutionPair { + source_config: config.clone(), + target_config: config, + }, + ) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/minimumdominatingset_minimumsetcovering.rs"] +mod tests; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 7606f4923..94aec5fc8 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -114,6 +114,7 @@ pub(crate) mod minimumcostmaximumflow_minimumcostcirculation; pub(crate) mod minimumcoveringbycliques_minimumintersectiongraphbasis; pub(crate) mod minimumdiscreteplanarinversekinematics_qubo; mod minimumdominatingset_casts; +pub(crate) mod minimumdominatingset_minimumsetcovering; mod minimumfeedbackvertexset_casts; pub(crate) mod minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters; pub(crate) mod minimummaximalmatching_maximumachromaticnumber; @@ -485,6 +486,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec MinimumDominatingSet { + MinimumDominatingSet::new( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + vec![3, 1, 4, 1, 3], + ) +} + +fn configs(n: usize) -> impl Iterator> { + (0..1usize << n).map(move |mask| (0..n).map(|v| mask & (1 << v) != 0).collect()) +} + +#[test] +fn test_minimumdominatingset_to_minimumsetcovering_closed_loop() { + let source = weighted_path(); + let result = ReduceTo::>::reduce_to(&source).unwrap(); + let target = ReductionResult::target_problem(&result); + assert_eq!(target.universe_size(), 5); + assert_eq!(target.num_sets(), 5); + assert_eq!( + target.sets(), + &[ + vec![0, 1], + vec![0, 1, 2], + vec![1, 2, 3], + vec![2, 3, 4], + vec![3, 4] + ] + ); + assert_eq!(target.weights_ref(), &[3, 1, 4, 1, 3]); + let optimum = vec![false, true, false, true, false]; + assert_eq!( + BruteForce::new().find_all_witnesses(target).unwrap(), + vec![optimum.clone()] + ); + assert_eq!(source.evaluate(&optimum).unwrap(), Min(Some(2))); + assert_eq!(result.extract_solution(&optimum).unwrap(), optimum); + let target_optimum = target + .evaluate(&BruteForce::new().solve(target).unwrap().unwrap()) + .unwrap(); + let source_optimum = source + .evaluate(&BruteForce::new().solve(&source).unwrap().unwrap()) + .unwrap(); + assert_eq!(result.extract_value(target_optimum), source_optimum); + for config in [ + vec![true, false, false, true, false], + vec![false, true, false, false, true], + ] { + assert_eq!(target.evaluate(&config).unwrap(), Min(Some(4))); + assert_eq!( + source + .evaluate(&result.extract_solution(&config).unwrap()) + .unwrap(), + Min(Some(4)) + ); + } + assert_parameter_predictions(&source, &result); + assert_optimization_round_trip_from_optimization_target(&source, &result, "weighted path"); +} + +#[test] +fn test_no_cover_within_bound() { + let source = weighted_path(); + let result = ReduceTo::>::reduce_to(&source).unwrap(); + let target = ReductionResult::target_problem(&result); + assert!( + BruteForce::new() + .solve(&Decision::new(source, 1)) + .unwrap() + .is_none() + ); + for config in configs(5) { + assert!( + target + .evaluate(&config) + .unwrap() + .0 + .is_none_or(|cost| cost > 1) + ); + } + assert_eq!( + target + .evaluate(&vec![false, true, false, true, false]) + .unwrap(), + Min(Some(2)) + ); +} + +fn exhaustive(n: usize) { + let pairs: Vec<_> = (0..n) + .flat_map(|u| (u + 1..n).map(move |v| (u, v))) + .collect(); + for edge_mask in 0..1usize << pairs.len() { + let edges: Vec<_> = pairs + .iter() + .enumerate() + .filter(|(i, _)| edge_mask & (1 << i) != 0) + .map(|(_, &e)| e) + .collect(); + for mut weight_code in 0..3usize.pow(u32::try_from(n).unwrap()) { + let weights = (0..n) + .map(|_| { + let weight = [-1, 0, 1][weight_code % 3]; + weight_code /= 3; + weight + }) + .collect(); + let source = MinimumDominatingSet::new(SimpleGraph::new(n, edges.clone()), weights); + let result = ReduceTo::>::reduce_to(&source).unwrap(); + let target = ReductionResult::target_problem(&result); + assert_parameter_predictions(&source, &result); + let mut feasible = Vec::new(); + for config in configs(n) { + let expected = source.evaluate(&config).unwrap(); + let actual = target.evaluate(&config).unwrap(); + assert_eq!( + actual, + expected, + "edges={edges:?}, weights={:?}, config={config:?}", + source.weights() + ); + if let Some(cost) = actual.0 { + assert_eq!(result.extract_solution(&config).unwrap(), config); + feasible.push((config, cost)); + } else { + assert!(result.extract_solution(&config).is_err()); + } + } + let optimum = feasible.iter().map(|(_, cost)| *cost).min().unwrap(); + for (config, _) in feasible.iter().filter(|(_, cost)| *cost == optimum) { + assert_eq!( + source + .evaluate(&result.extract_solution(config).unwrap()) + .unwrap(), + Min(Some(optimum)) + ); + } + } + } +} + +#[test] +fn test_exhaustive_signed_graphs_through_two_vertices() { + for n in 0..=2 { + exhaustive(n); + } +} + +#[test] +fn test_exhaustive_signed_three_vertex_graphs() { + exhaustive(3); +} + +#[test] +fn test_exhaustive_signed_four_vertex_graphs() { + exhaustive(4); +} + +#[test] +fn test_empty_and_isolated_vertices() { + for weights in [vec![], vec![-5], vec![0, -5, 2]] { + let n = weights.len(); + let expected_cost = weights.iter().sum(); + let source = MinimumDominatingSet::new(SimpleGraph::empty(n), weights); + let result = ReduceTo::>::reduce_to(&source).unwrap(); + let target = ReductionResult::target_problem(&result); + assert_eq!(target.sets(), (0..n).map(|v| vec![v]).collect::>()); + assert_eq!( + target.evaluate(&vec![true; n]).unwrap(), + Min(Some(expected_cost)) + ); + assert_eq!( + BruteForce::new().find_all_witnesses(target).unwrap(), + vec![vec![true; n]] + ); + assert_parameter_predictions(&source, &result); + assert_optimization_round_trip_from_optimization_target( + &source, + &result, + "isolated vertices", + ); + } +} + +#[test] +fn test_equal_neighborhoods_and_disconnected_components() { + for n in [3, 4] { + let weights = if n == 3 { + vec![-5, 0, 2] + } else { + vec![-5, 0, 2, -3] + }; + let source = + MinimumDominatingSet::new(SimpleGraph::new(n, vec![(0, 1), (0, 2), (1, 2)]), weights); + let result = ReduceTo::>::reduce_to(&source).unwrap(); + let target = ReductionResult::target_problem(&result); + assert_eq!(&target.sets()[..3], &vec![vec![0, 1, 2]; 3]); + assert_eq!(target.num_sets(), n); + let expected = if n == 3 { + vec![vec![true, false, false], vec![true, true, false]] + } else { + vec![ + vec![true, false, false, true], + vec![true, true, false, true], + ] + }; + let mut actual = BruteForce::new().find_all_witnesses(target).unwrap(); + actual.sort(); + assert_eq!(actual, expected); + assert_optimization_round_trip_from_optimization_target( + &source, + &result, + "equal neighborhoods", + ); + } +} + +#[test] +fn test_signed_boundaries_and_overflow_before_cancellation() { + for (weights, expected) in [ + (vec![i64::MIN, 0, i64::MAX], Some(-1)), + (vec![i64::MIN, 0, 0], Some(i64::MIN)), + (vec![i64::MAX, 0, 0], Some(i64::MAX)), + (vec![i64::MAX, 1, -1], None), + (vec![i64::MIN, -1, 1], None), + ] { + let source = + MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), weights.clone()); + let result = ReduceTo::>::reduce_to(&source).unwrap(); + let target = ReductionResult::target_problem(&result); + assert_eq!(target.weights_ref(), weights); + let config = vec![true; 3]; + if let Some(cost) = expected { + assert_eq!(source.evaluate(&config).unwrap(), Min(Some(cost))); + assert_eq!(target.evaluate(&config).unwrap(), Min(Some(cost))); + assert_eq!(result.extract_solution(&config).unwrap(), config); + } else { + assert!(matches!( + source.evaluate(&config), + Err(EvaluationError::IntegerOverflow(_)) + )); + assert!(matches!( + target.evaluate(&config), + Err(EvaluationError::IntegerOverflow(_)) + )); + assert!(matches!( + result.extract_solution(&config), + Err(ExtractionError::Evaluation( + EvaluationError::IntegerOverflow(_) + )) + )); + } + for value in [Min(None), Min(Some(i64::MIN)), Min(Some(i64::MAX))] { + assert_eq!(result.extract_value(value), value); + } + } + let source = MinimumDominatingSet::new(SimpleGraph::empty(3), vec![i64::MAX, 1, -1]); + let result = ReduceTo::>::reduce_to(&source).unwrap(); + let config = vec![true, true, false]; + assert_eq!(source.evaluate(&config).unwrap(), Min(None)); + assert_eq!( + ReductionResult::target_problem(&result) + .evaluate(&config) + .unwrap(), + Min(None) + ); + assert!(result.extract_solution(&config).is_err()); +} + +#[test] +fn test_extraction_rejects_non_covers_and_wrong_lengths() { + let result = ReduceTo::>::reduce_to(&weighted_path()).unwrap(); + for config in [vec![false; 5], vec![false, true, false, false, false]] { + assert!(matches!( + result.extract_solution(&config), + Err(ExtractionError::InvalidTargetSolution(_)) + )); + } + for config in [vec![true; 4], vec![true; 6]] { + assert!(matches!( + result.extract_solution(&config), + Err(ExtractionError::Evaluation( + EvaluationError::InvalidConfiguration(_) + )) + )); + } +} + +#[test] +fn test_simple_graph_rejects_loops_and_repeated_edges() { + for edges in [vec![(0, 0)], vec![(0, 1), (0, 1)], vec![(0, 1), (1, 0)]] { + assert!(SimpleGraph::try_new(3, edges.clone()).is_err()); + let source = serde_json::json!({"graph": {"num_vertices": 3, "edges": edges}, "weights": [0, -5, 2]}); + assert!(serde_json::from_value::>(source).is_err()); + } +}