diff --git a/docs/paper/examples/maximumindependentset_to_maximumclique.json b/docs/paper/examples/maximumindependentset_to_maximumclique.json index 21a8853eb..352ddeb68 100644 --- a/docs/paper/examples/maximumindependentset_to_maximumclique.json +++ b/docs/paper/examples/maximumindependentset_to_maximumclique.json @@ -31,8 +31,8 @@ "target": { "problem": "MaximumClique", "variant": { - "weight": "i32", - "graph": "SimpleGraph" + "graph": "SimpleGraph", + "weight": "i32" }, "instance": { "edges": [ diff --git a/docs/paper/examples/travelingsalesman_to_qubo.json b/docs/paper/examples/travelingsalesman_to_qubo.json index c063cacdc..c9aecf52d 100644 --- a/docs/paper/examples/travelingsalesman_to_qubo.json +++ b/docs/paper/examples/travelingsalesman_to_qubo.json @@ -2,8 +2,8 @@ "source": { "problem": "TravelingSalesman", "variant": { - "weight": "i32", - "graph": "SimpleGraph" + "graph": "SimpleGraph", + "weight": "i32" }, "instance": { "num_edges": 3, diff --git a/examples/detect_unreachable_from_3sat.rs b/examples/detect_unreachable_from_3sat.rs index c48672276..b770ae6e9 100644 --- a/examples/detect_unreachable_from_3sat.rs +++ b/examples/detect_unreachable_from_3sat.rs @@ -109,8 +109,7 @@ fn main() { // Check if ALL variants of this problem are P-time // (conservative: if any variant could be hard, don't classify as P) let variants = graph.variants_for(name); - variants.len() == 1 - && variants[0].get(*key).map(|s| s.as_str()) == Some(*val) + variants.len() == 1 && variants[0].get(*key).map(|s| s.as_str()) == Some(*val) } } }); @@ -143,10 +142,7 @@ fn main() { } if !p_time.is_empty() { - println!( - "In P — correctly unreachable ({}):", - p_time.len() - ); + println!("In P — correctly unreachable ({}):", p_time.len()); for name in &p_time { println!(" {name}"); } @@ -165,10 +161,7 @@ fn main() { } if !orphans.is_empty() { - println!( - "Orphans — no reductions at all ({}):", - orphans.len() - ); + println!("Orphans — no reductions at all ({}):", orphans.len()); for name in &orphans { println!(" {name}"); } diff --git a/examples/reduction_binpacking_to_ilp.rs b/examples/reduction_binpacking_to_ilp.rs index 104d707a4..74b87e1bd 100644 --- a/examples/reduction_binpacking_to_ilp.rs +++ b/examples/reduction_binpacking_to_ilp.rs @@ -53,7 +53,10 @@ pub fn run() { // 5. Extract source solution let bp_solution = reduction.extract_solution(&ilp_solution); - println!("Source BinPacking solution (bin assignments): {:?}", bp_solution); + println!( + "Source BinPacking solution (bin assignments): {:?}", + bp_solution + ); // 6. Verify let size = bp.evaluate(&bp_solution); @@ -76,13 +79,8 @@ pub fn run() { let source_variant = variant_to_map(BinPacking::::variant()); let target_variant = variant_to_map(ILP::::variant()); - let overhead = lookup_overhead( - "BinPacking", - &source_variant, - "ILP", - &target_variant, - ) - .unwrap_or_default(); + let overhead = + lookup_overhead("BinPacking", &source_variant, "ILP", &target_variant).unwrap_or_default(); let data = ReductionData { source: ProblemSide { diff --git a/examples/reduction_longestcommonsubsequence_to_ilp.rs b/examples/reduction_longestcommonsubsequence_to_ilp.rs index 5ff1529ac..edaad1d5e 100644 --- a/examples/reduction_longestcommonsubsequence_to_ilp.rs +++ b/examples/reduction_longestcommonsubsequence_to_ilp.rs @@ -73,9 +73,13 @@ pub fn run() { let source_variant = variant_to_map(LongestCommonSubsequence::variant()); let target_variant = variant_to_map(ILP::::variant()); - let overhead = - lookup_overhead("LongestCommonSubsequence", &source_variant, "ILP", &target_variant) - .expect("LCS -> ILP overhead not found"); + let overhead = lookup_overhead( + "LongestCommonSubsequence", + &source_variant, + "ILP", + &target_variant, + ) + .expect("LCS -> ILP overhead not found"); let data = ReductionData { source: ProblemSide { diff --git a/pipeline-output.log b/pipeline-output.log new file mode 100644 index 000000000..34c61dc74 --- /dev/null +++ b/pipeline-output.log @@ -0,0 +1,11 @@ + + +The structural reviewer also completed — all 16 model checks and 14 rule checks passed with no critical issues. The pipeline for issue #126 is complete. + +``` +Pipeline complete: + Issue: #126 [Rule] KSatisfiability to SubsetSum + PR: #599 (https://github.com/CodingThrust/problem-reductions/pull/599) + Status: Implemented, Copilot review requested + Board: Ready -> In Progress -> review-agentic +``` diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 2eee89aca..6b4cbb5be 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -211,6 +211,7 @@ Flags by problem type: GraphPartitioning --graph Factoring --target, --m, --n BinPacking --sizes, --capacity + SubsetSum --sizes, --target PaintShop --sequence MaximumSetPacking --sets [--weights] MinimumSetCovering --universe, --sets [--weights] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index fae9adcd1..2b4cb04b3 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -6,7 +6,7 @@ use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::GraphPartitioning; -use problemreductions::models::misc::{BinPacking, LongestCommonSubsequence, PaintShop}; +use problemreductions::models::misc::{BinPacking, LongestCommonSubsequence, PaintShop, SubsetSum}; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; use problemreductions::topology::{ @@ -63,6 +63,8 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { "Vec>" => "semicolon-separated rows: \"1,0.5;0.5,2\"", "usize" => "integer", "u64" => "integer", + "i64" => "integer", + "Vec" => "comma-separated integers: 3,7,1,8", _ => "value", } } @@ -88,6 +90,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "SpinGlass" => "--graph 0-1,1-2 --couplings 1,1", "KColoring" => "--graph 0-1,1-2,2-0 --k 3", "Factoring" => "--target 15 --m 4 --n 4", + "SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11", _ => "", } } @@ -351,6 +354,27 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { } } + // SubsetSum + "SubsetSum" => { + let sizes_str = args.sizes.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "SubsetSum requires --sizes and --target\n\n\ + Usage: pred create SubsetSum --sizes 3,7,1,8,2,4 --target 11" + ) + })?; + let target = args.target.ok_or_else(|| { + anyhow::anyhow!( + "SubsetSum requires --target\n\n\ + Usage: pred create SubsetSum --sizes 3,7,1,8,2,4 --target 11" + ) + })?; + let sizes: Vec = util::parse_comma_list(sizes_str)?; + ( + ser(SubsetSum::new(sizes, target as i64))?, + resolved_variant.clone(), + ) + } + // PaintShop "PaintShop" => { let seq_str = args.sequence.as_deref().ok_or_else(|| { diff --git a/review-output.log b/review-output.log new file mode 100644 index 000000000..cdcfaa77b --- /dev/null +++ b/review-output.log @@ -0,0 +1,10 @@ +**PR #594 (Fix #129: Add MultivariateQuadratic model) processed successfully.** + +| Check | Result | +|-------|--------| +| Merge conflicts | 2 files resolved | +| Copilot comments | 0 actionable (already fixed) | +| Issue/human comments | 3 checked, 0 fixes needed | +| CI | green | +| Agentic test | passed | +| Board | review-agentic → In Review | diff --git a/src/lib.rs b/src/lib.rs index ef6754138..bdcbf5f32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,9 @@ pub mod prelude { KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, TravelingSalesman, }; - pub use crate::models::misc::{BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum}; + pub use crate::models::misc::{ + BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum, + }; pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering}; // Core traits diff --git a/src/rules/longestcommonsubsequence_ilp.rs b/src/rules/longestcommonsubsequence_ilp.rs index a69610a35..4634045bb 100644 --- a/src/rules/longestcommonsubsequence_ilp.rs +++ b/src/rules/longestcommonsubsequence_ilp.rs @@ -57,13 +57,7 @@ impl ReductionResult for ReductionLCSToILP { .iter() .enumerate() .filter(|(i, _)| target_solution.get(*i).copied().unwrap_or(0) == 1) - .map(|(_, &(j1, j2))| { - if shortest_is_first { - j1 - } else { - j2 - } - }) + .map(|(_, &(j1, j2))| if shortest_is_first { j1 } else { j2 }) .collect(); let mut config = vec![0usize; shortest_len]; @@ -141,17 +135,11 @@ impl ReduceTo> for LongestCommonSubsequence { for (i, &(j1, j2)) in match_pairs.iter().enumerate() { for (k, &(j1p, j2p)) in match_pairs.iter().enumerate() { if i < k && j1 < j1p && j2 > j2p { - constraints.push(LinearConstraint::le( - vec![(i, 1.0), (k, 1.0)], - 1.0, - )); + constraints.push(LinearConstraint::le(vec![(i, 1.0), (k, 1.0)], 1.0)); } // Also check the reverse: j1 > j1p and j2 < j2p if i < k && j1 > j1p && j2 < j2p { - constraints.push(LinearConstraint::le( - vec![(i, 1.0), (k, 1.0)], - 1.0, - )); + constraints.push(LinearConstraint::le(vec![(i, 1.0), (k, 1.0)], 1.0)); } } } diff --git a/src/unit_tests/models/misc/longest_common_subsequence.rs b/src/unit_tests/models/misc/longest_common_subsequence.rs index e9059ddaf..33d4a0e90 100644 --- a/src/unit_tests/models/misc/longest_common_subsequence.rs +++ b/src/unit_tests/models/misc/longest_common_subsequence.rs @@ -14,7 +14,10 @@ fn test_lcs_basic() { assert_eq!(problem.num_chars_first(), 6); assert_eq!(problem.num_chars_second(), 6); assert_eq!(problem.direction(), Direction::Maximize); - assert_eq!(::NAME, "LongestCommonSubsequence"); + assert_eq!( + ::NAME, + "LongestCommonSubsequence" + ); assert_eq!(::variant(), vec![]); } @@ -32,10 +35,8 @@ fn test_lcs_dims() { fn test_lcs_evaluate_valid() { // s1 = "ABC", s2 = "ACB" // Selecting positions 0,2 of s1 (shorter) gives "AC" which is subseq of "ACB" - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B', b'C'], - vec![b'A', b'C', b'B'], - ]); + let problem = + LongestCommonSubsequence::new(vec![vec![b'A', b'B', b'C'], vec![b'A', b'C', b'B']]); let result = problem.evaluate(&[1, 0, 1]); // "AC" assert!(result.is_valid()); assert_eq!(result.unwrap(), 2); @@ -45,20 +46,16 @@ fn test_lcs_evaluate_valid() { fn test_lcs_evaluate_invalid_subsequence() { // s1 = "ABC", s2 = "CAB" // Selecting positions 1,2 of s1 gives "BC" - is "BC" a subseq of "CAB"? No - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B', b'C'], - vec![b'C', b'A', b'B'], - ]); + let problem = + LongestCommonSubsequence::new(vec![vec![b'A', b'B', b'C'], vec![b'C', b'A', b'B']]); let result = problem.evaluate(&[0, 1, 1]); // "BC" assert!(!result.is_valid()); } #[test] fn test_lcs_evaluate_empty_selection() { - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B', b'C'], - vec![b'X', b'Y', b'Z'], - ]); + let problem = + LongestCommonSubsequence::new(vec![vec![b'A', b'B', b'C'], vec![b'X', b'Y', b'Z']]); let result = problem.evaluate(&[0, 0, 0]); // empty assert!(result.is_valid()); assert_eq!(result.unwrap(), 0); @@ -66,20 +63,14 @@ fn test_lcs_evaluate_empty_selection() { #[test] fn test_lcs_evaluate_wrong_config_length() { - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B'], - vec![b'A', b'B', b'C'], - ]); + let problem = LongestCommonSubsequence::new(vec![vec![b'A', b'B'], vec![b'A', b'B', b'C']]); assert!(!problem.evaluate(&[1]).is_valid()); assert!(!problem.evaluate(&[1, 0, 0]).is_valid()); } #[test] fn test_lcs_evaluate_invalid_variable_value() { - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B'], - vec![b'A', b'B'], - ]); + let problem = LongestCommonSubsequence::new(vec![vec![b'A', b'B'], vec![b'A', b'B']]); assert!(!problem.evaluate(&[2, 0]).is_valid()); } @@ -100,10 +91,8 @@ fn test_lcs_brute_force_two_strings() { #[test] fn test_lcs_identical_strings() { - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B', b'C'], - vec![b'A', b'B', b'C'], - ]); + let problem = + LongestCommonSubsequence::new(vec![vec![b'A', b'B', b'C'], vec![b'A', b'B', b'C']]); let solver = BruteForce::new(); let solution = solver.find_best(&problem).expect("should find a solution"); let metric = problem.evaluate(&solution); @@ -112,10 +101,7 @@ fn test_lcs_identical_strings() { #[test] fn test_lcs_no_common_chars() { - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B'], - vec![b'C', b'D'], - ]); + let problem = LongestCommonSubsequence::new(vec![vec![b'A', b'B'], vec![b'C', b'D']]); let solver = BruteForce::new(); let solution = solver.find_best(&problem).expect("should find a solution"); let metric = problem.evaluate(&solution); @@ -125,10 +111,7 @@ fn test_lcs_no_common_chars() { #[test] fn test_lcs_single_char_alphabet() { // All same character - LCS is length of shortest string - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'A', b'A'], - vec![b'A', b'A'], - ]); + let problem = LongestCommonSubsequence::new(vec![vec![b'A', b'A', b'A'], vec![b'A', b'A']]); let solver = BruteForce::new(); let solution = solver.find_best(&problem).expect("should find a solution"); let metric = problem.evaluate(&solution); @@ -152,10 +135,8 @@ fn test_lcs_three_strings() { #[test] fn test_lcs_serialization() { - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B', b'C'], - vec![b'A', b'C', b'B'], - ]); + let problem = + LongestCommonSubsequence::new(vec![vec![b'A', b'B', b'C'], vec![b'A', b'C', b'B']]); let json = serde_json::to_value(&problem).unwrap(); let restored: LongestCommonSubsequence = serde_json::from_value(json).unwrap(); assert_eq!(restored.strings(), problem.strings()); @@ -163,10 +144,7 @@ fn test_lcs_serialization() { #[test] fn test_lcs_empty_string_in_input() { - let problem = LongestCommonSubsequence::new(vec![ - vec![], - vec![b'A', b'B', b'C'], - ]); + let problem = LongestCommonSubsequence::new(vec![vec![], vec![b'A', b'B', b'C']]); assert_eq!(problem.dims(), Vec::::new()); assert!(problem.evaluate(&[]).is_valid()); assert_eq!(problem.evaluate(&[]).unwrap(), 0); diff --git a/src/unit_tests/models/misc/subset_sum.rs b/src/unit_tests/models/misc/subset_sum.rs index 5fa113c6b..78dbb2747 100644 --- a/src/unit_tests/models/misc/subset_sum.rs +++ b/src/unit_tests/models/misc/subset_sum.rs @@ -29,6 +29,8 @@ fn test_subsetsum_evaluate_unsatisfying() { assert!(!problem.evaluate(&[1, 1, 0, 0, 0, 0])); // empty = 0 ≠ 11 assert!(!problem.evaluate(&[0, 0, 0, 0, 0, 0])); + // all = 25 ≠ 11 + assert!(!problem.evaluate(&[1, 1, 1, 1, 1, 1])); } #[test] @@ -105,3 +107,18 @@ fn test_subsetsum_single_element() { assert!(problem.evaluate(&[1])); assert!(!problem.evaluate(&[0])); } + +#[test] +fn test_subsetsum_all_selected() { + // Target equals sum of all elements + let problem = SubsetSum::new(vec![1, 2, 3, 4], 10); + assert!(problem.evaluate(&[1, 1, 1, 1])); // 1+2+3+4 = 10 +} + +#[test] +fn test_subsetsum_target_zero() { + // Target 0 with non-empty set: only empty subset works + let problem = SubsetSum::new(vec![1, 2, 3], 0); + assert!(problem.evaluate(&[0, 0, 0])); // empty subset sums to 0 + assert!(!problem.evaluate(&[1, 0, 0])); // 1 != 0 +} diff --git a/src/unit_tests/rules/longestcommonsubsequence_ilp.rs b/src/unit_tests/rules/longestcommonsubsequence_ilp.rs index 101da6b99..7762d2349 100644 --- a/src/unit_tests/rules/longestcommonsubsequence_ilp.rs +++ b/src/unit_tests/rules/longestcommonsubsequence_ilp.rs @@ -9,8 +9,7 @@ fn test_lcs_to_ilp_issue_example() { vec![b'A', b'B', b'A', b'C'], vec![b'B', b'A', b'C', b'A'], ]); - let reduction: ReductionLCSToILP = - ReduceTo::>::reduce_to(&problem); + let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); // 6 match pairs as described in the issue @@ -45,8 +44,7 @@ fn test_lcs_to_ilp_closed_loop() { let bf_value = bf_metric.unwrap(); // ILP optimal - let reduction: ReductionLCSToILP = - ReduceTo::>::reduce_to(&problem); + let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); @@ -66,12 +64,9 @@ fn test_lcs_to_ilp_closed_loop() { #[test] fn test_lcs_to_ilp_identical_strings() { // LCS of identical strings = the string itself - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B', b'C'], - vec![b'A', b'B', b'C'], - ]); - let reduction: ReductionLCSToILP = - ReduceTo::>::reduce_to(&problem); + let problem = + LongestCommonSubsequence::new(vec![vec![b'A', b'B', b'C'], vec![b'A', b'B', b'C']]); + let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -85,12 +80,8 @@ fn test_lcs_to_ilp_identical_strings() { #[test] fn test_lcs_to_ilp_no_common_chars() { - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B'], - vec![b'C', b'D'], - ]); - let reduction: ReductionLCSToILP = - ReduceTo::>::reduce_to(&problem); + let problem = LongestCommonSubsequence::new(vec![vec![b'A', b'B'], vec![b'C', b'D']]); + let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); // No match pairs → 0 variables @@ -108,12 +99,8 @@ fn test_lcs_to_ilp_no_common_chars() { #[test] fn test_lcs_to_ilp_single_char_alphabet() { // All same chars → LCS = min length - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'A', b'A'], - vec![b'A', b'A'], - ]); - let reduction: ReductionLCSToILP = - ReduceTo::>::reduce_to(&problem); + let problem = LongestCommonSubsequence::new(vec![vec![b'A', b'A', b'A'], vec![b'A', b'A']]); + let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -128,10 +115,8 @@ fn test_lcs_to_ilp_single_char_alphabet() { #[test] fn test_lcs_to_ilp_asymmetric_lengths() { // s1 = "AB", s2 = "AABB" - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B'], - vec![b'A', b'A', b'B', b'B'], - ]); + let problem = + LongestCommonSubsequence::new(vec![vec![b'A', b'B'], vec![b'A', b'A', b'B', b'B']]); // BF optimal let bf = BruteForce::new(); @@ -139,8 +124,7 @@ fn test_lcs_to_ilp_asymmetric_lengths() { let bf_value = problem.evaluate(&bf_solution).unwrap(); // ILP optimal - let reduction: ReductionLCSToILP = - ReduceTo::>::reduce_to(&problem); + let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).unwrap(); @@ -154,12 +138,8 @@ fn test_lcs_to_ilp_asymmetric_lengths() { #[test] fn test_lcs_to_ilp_constraint_structure() { // Verify basic ILP structure for a small example - let problem = LongestCommonSubsequence::new(vec![ - vec![b'A', b'B'], - vec![b'B', b'A'], - ]); - let reduction: ReductionLCSToILP = - ReduceTo::>::reduce_to(&problem); + let problem = LongestCommonSubsequence::new(vec![vec![b'A', b'B'], vec![b'B', b'A']]); + let reduction: ReductionLCSToILP = ReduceTo::>::reduce_to(&problem); let ilp = reduction.target_problem(); // Match pairs: (0,1)=A, (1,0)=B → 2 variables