Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
"target": {
"problem": "MaximumClique",
"variant": {
"weight": "i32",
"graph": "SimpleGraph"
"graph": "SimpleGraph",
"weight": "i32"
},
"instance": {
"edges": [
Expand Down
4 changes: 2 additions & 2 deletions docs/paper/examples/travelingsalesman_to_qubo.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"source": {
"problem": "TravelingSalesman",
"variant": {
"weight": "i32",
"graph": "SimpleGraph"
"graph": "SimpleGraph",
"weight": "i32"
},
"instance": {
"num_edges": 3,
Expand Down
13 changes: 3 additions & 10 deletions examples/detect_unreachable_from_3sat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
});
Expand Down Expand Up @@ -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}");
}
Expand All @@ -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}");
}
Expand Down
14 changes: 6 additions & 8 deletions examples/reduction_binpacking_to_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -76,13 +79,8 @@ pub fn run() {

let source_variant = variant_to_map(BinPacking::<i32>::variant());
let target_variant = variant_to_map(ILP::<bool>::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 {
Expand Down
10 changes: 7 additions & 3 deletions examples/reduction_longestcommonsubsequence_to_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,13 @@ pub fn run() {

let source_variant = variant_to_map(LongestCommonSubsequence::variant());
let target_variant = variant_to_map(ILP::<bool>::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 {
Expand Down
11 changes: 11 additions & 0 deletions pipeline-output.log
Original file line number Diff line number Diff line change
@@ -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
```
1 change: 1 addition & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
26 changes: 25 additions & 1 deletion problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -63,6 +63,8 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
"Vec<Vec<W>>" => "semicolon-separated rows: \"1,0.5;0.5,2\"",
"usize" => "integer",
"u64" => "integer",
"i64" => "integer",
"Vec<i64>" => "comma-separated integers: 3,7,1,8",
_ => "value",
}
}
Expand All @@ -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",
_ => "",
}
}
Expand Down Expand Up @@ -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<i64> = 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(|| {
Expand Down
10 changes: 10 additions & 0 deletions review-output.log
Original file line number Diff line number Diff line change
@@ -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 |
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 3 additions & 15 deletions src/rules/longestcommonsubsequence_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -141,17 +135,11 @@ impl ReduceTo<ILP<bool>> 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));
}
}
}
Expand Down
60 changes: 19 additions & 41 deletions src/unit_tests/models/misc/longest_common_subsequence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(<LongestCommonSubsequence as Problem>::NAME, "LongestCommonSubsequence");
assert_eq!(
<LongestCommonSubsequence as Problem>::NAME,
"LongestCommonSubsequence"
);
assert_eq!(<LongestCommonSubsequence as Problem>::variant(), vec![]);
}

Expand All @@ -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);
Expand All @@ -45,41 +46,31 @@ 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);
}

#[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());
}

Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -152,21 +135,16 @@ 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());
}

#[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::<usize>::new());
assert!(problem.evaluate(&[]).is_valid());
assert_eq!(problem.evaluate(&[]).unwrap(), 0);
Expand Down
17 changes: 17 additions & 0 deletions src/unit_tests/models/misc/subset_sum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
}
Loading
Loading