diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md index 05e2c471f..6b7db80c6 100644 --- a/.claude/skills/add-model/SKILL.md +++ b/.claude/skills/add-model/SKILL.md @@ -57,7 +57,7 @@ Create `src/models//.rs`: // 1. inventory::submit! for ProblemSchemaEntry // 2. Struct definition with #[derive(Debug, Clone, Serialize, Deserialize)] // 3. Constructor (new) + accessor methods -// 4. Problem trait impl (NAME, Metric, dims, evaluate, variant, problem_size_names, problem_size_values) +// 4. Problem trait impl (NAME, Metric, dims, evaluate, variant) // 5. OptimizationProblem or SatisfactionProblem impl // 6. #[cfg(test)] #[path = "..."] mod tests; ``` diff --git a/.claude/skills/add-rule/SKILL.md b/.claude/skills/add-rule/SKILL.md index 4010608ca..a54e85d87 100644 --- a/.claude/skills/add-rule/SKILL.md +++ b/.claude/skills/add-rule/SKILL.md @@ -20,7 +20,7 @@ Before any implementation, collect all required information. If called from `iss | 3 | **Reduction algorithm** | How to transform source instance to target | "Copy graph and weights; IS on same graph as VC" | | 4 | **Solution extraction** | How to map target solution back to source | "Complement: `1 - x` for each variable" | | 5 | **Correctness argument** | Why the reduction preserves optimality | "S is independent set iff V\S is vertex cover" | -| 6 | **Size overhead** | How target size relates to source size | `num_vertices: poly!(num_vertices), num_edges: poly!(num_edges)` | +| 6 | **Size overhead** | How target size relates to source size | `num_vertices = "num_vertices", num_edges = "num_edges"` | | 7 | **Concrete example** | A small worked-out instance (tutorial style, clear intuition) | "Triangle graph: VC={0,1} -> IS={2}" | | 8 | **Solving strategy** | How to solve the target problem | "BruteForce, or existing ILP reduction" | | 9 | **Reference** | Paper, textbook, or URL for the reduction | URL or citation | @@ -75,13 +75,9 @@ impl ReductionResult for ReductionXToY { **ReduceTo with `#[reduction]` macro:** ```rust -#[reduction( - overhead = { - ReductionOverhead::new(vec![ - ("field_name", poly!(source_field)), - ]) - } -)] +#[reduction(overhead = { + field_name = "source_field", +})] impl ReduceTo for SourceType { type Result = ReductionXToY; fn reduce_to(&self) -> Self::Result { ... } @@ -180,7 +176,7 @@ Adding a reduction rule does NOT require CLI changes -- the reduction graph is a | Mistake | Fix | |---------|-----| | Forgetting `#[reduction(...)]` macro | Required for compile-time registration in the reduction graph | -| Wrong overhead polynomial | Must accurately reflect the size relationship | +| Wrong overhead expression | Must accurately reflect the size relationship | | Missing `extract_solution` mapping state | Store any index maps needed in the ReductionResult struct | | Example missing `pub fn run()` | Required for the test harness (`include!` pattern) | | Not registering example in `tests/suites/examples.rs` | Must add both `example_test!` and `example_fn!` | diff --git a/.claude/skills/review-implementation/SKILL.md b/.claude/skills/review-implementation/SKILL.md index 3e543acfd..886116961 100644 --- a/.claude/skills/review-implementation/SKILL.md +++ b/.claude/skills/review-implementation/SKILL.md @@ -88,12 +88,12 @@ Read the implementation files and assess: ### For Models: 1. **`evaluate()` correctness** -- Does it check feasibility before computing the objective? Does it return `SolutionSize::Invalid` / `false` for infeasible configs? 2. **`dims()` correctness** -- Does it return the actual configuration space? (e.g., `vec![2; n]` for binary) -3. **`problem_size_names`/`problem_size_values` consistency** -- Do the names match what `ReductionOverhead` uses? +3. **Size getter consistency** -- Do the inherent getter methods (e.g., `num_vertices()`, `num_edges()`) match names used in overhead expressions? 4. **Weight handling** -- Are weights managed via inherent methods, not traits? ### For Rules: 1. **`extract_solution` correctness** -- Does it correctly invert the reduction? Does the returned solution have the right length (source dimensions)? -2. **Overhead accuracy** -- Does `poly!(...)` reflect the actual size relationship? +2. **Overhead accuracy** -- Does the `overhead = { field = "expr" }` reflect the actual size relationship? 3. **Example quality** -- Is it tutorial-style? Does it use the instance from the issue? Does the JSON export include both source and target data? 4. **Paper quality** -- Is the reduction-rule statement precise? Is the proof sketch sound? Is the example figure clear? diff --git a/.github/ISSUE_TEMPLATE/problem.md b/.github/ISSUE_TEMPLATE/problem.md index 3d7903f9c..f1af2249a 100644 --- a/.github/ISSUE_TEMPLATE/problem.md +++ b/.github/ISSUE_TEMPLATE/problem.md @@ -54,7 +54,7 @@ Connect fields to the symbols defined above. +Also provide the code-level metric name (matching the problem's inherent getter methods, e.g., num_vertices, num_edges). --> | Target metric (code name) | Polynomial (using symbols above) | |----------------------------|----------------------------------| diff --git a/docs/src/design.md b/docs/src/design.md index 6933a9eb9..ef8ce6e84 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -184,14 +184,10 @@ impl ReductionResult for ReductionISToVC { The `#[reduction]` attribute on the `ReduceTo` impl registers the reduction in the global registry (via `inventory`): ```rust,ignore -#[reduction( - overhead = { - ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_vertices)), - ("num_edges", poly!(num_edges)), - ]) - } -)] +#[reduction(overhead = { + num_vertices = "num_vertices", + num_edges = "num_edges", +})] impl ReduceTo> for MaximumIndependentSet { @@ -212,10 +208,12 @@ inventory::submit! { target_name: "MinimumVertexCover", source_variant_fn: || as Problem>::variant(), target_variant_fn: || as Problem>::variant(), - overhead_fn: || ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_vertices)), - ("num_edges", poly!(num_edges)), - ]), + overhead_fn: || ReductionOverhead { + output_size: vec![ + ("num_vertices", Expr::Var("num_vertices")), + ("num_edges", Expr::Var("num_edges")), + ], + }, module_path: module_path!(), reduce_fn: |src: &dyn Any| -> Box { let src = src.downcast_ref::>().unwrap(); @@ -296,23 +294,17 @@ For full type control, you can also chain `ReduceTo::reduce_to()` calls manually
Overhead evaluation -Each reduction declares how the output problem size relates to the input, expressed as polynomials. The `poly!` macro provides concise syntax: +Each reduction declares how the output problem size relates to the input, expressed as symbolic `Expr` expressions. The `#[reduction]` macro parses overhead strings at compile time: ```rust,ignore -poly!(num_vertices) // p(x) = num_vertices -poly!(num_vertices ^ 2) // p(x) = num_vertices² -poly!(3 * num_edges) // p(x) = 3 × num_edges -poly!(num_vertices * num_edges) // p(x) = num_vertices × num_edges +#[reduction(overhead = { + num_vars = "num_vertices + num_edges", + num_clauses = "3 * num_edges", +})] +impl ReduceTo for Source { ... } ``` -A `ReductionOverhead` pairs output field names with their polynomials: - -```rust,ignore -ReductionOverhead::new(vec![ - ("num_vars", poly!(num_vertices) + poly!(num_edges)), - ("num_clauses", poly!(3 * num_edges)), -]) -``` +Expressions support: constants, variables, `+`, `*`, `^`, `exp()`, `log()`, `sqrt()`. Each problem type provides inherent getter methods (e.g., `num_vertices()`, `num_edges()`) that the overhead expressions reference. `evaluate_output_size(input)` substitutes input values: diff --git a/src/unit_tests/expr.rs b/src/unit_tests/expr.rs index d095882ed..71eb74ce5 100644 --- a/src/unit_tests/expr.rs +++ b/src/unit_tests/expr.rs @@ -141,4 +141,108 @@ fn test_expr_is_polynomial() { assert!(Expr::pow(Expr::Var("n"), Expr::Const(2.0)).is_polynomial()); assert!(!Expr::Exp(Box::new(Expr::Var("n"))).is_polynomial()); assert!(!Expr::Log(Box::new(Expr::Var("n"))).is_polynomial()); + assert!(!Expr::Sqrt(Box::new(Expr::Var("n"))).is_polynomial()); +} + +#[test] +fn test_expr_display_fractional_constant() { + assert_eq!(format!("{}", Expr::Const(2.75)), "2.75"); + assert_eq!(format!("{}", Expr::Const(0.5)), "0.5"); +} + +#[test] +fn test_expr_display_log() { + let e = Expr::Log(Box::new(Expr::Var("n"))); + assert_eq!(format!("{e}"), "log(n)"); +} + +#[test] +fn test_expr_display_sqrt() { + let e = Expr::Sqrt(Box::new(Expr::Var("n"))); + assert_eq!(format!("{e}"), "sqrt(n)"); +} + +#[test] +fn test_expr_display_mul_with_add_parenthesization() { + // (a + b) * c should parenthesize the left side + let e = Expr::mul(Expr::add(Expr::Var("a"), Expr::Var("b")), Expr::Var("c")); + assert_eq!(format!("{e}"), "(a + b) * c"); + + // c * (a + b) should parenthesize the right side + let e = Expr::mul(Expr::Var("c"), Expr::add(Expr::Var("a"), Expr::Var("b"))); + assert_eq!(format!("{e}"), "c * (a + b)"); + + // (a + b) * (c + d) should parenthesize both sides + let e = Expr::mul( + Expr::add(Expr::Var("a"), Expr::Var("b")), + Expr::add(Expr::Var("c"), Expr::Var("d")), + ); + assert_eq!(format!("{e}"), "(a + b) * (c + d)"); +} + +#[test] +fn test_expr_display_pow_with_complex_base() { + // (a + b)^2 + let e = Expr::pow(Expr::add(Expr::Var("a"), Expr::Var("b")), Expr::Const(2.0)); + assert_eq!(format!("{e}"), "(a + b)^2"); + + // (a * b)^2 + let e = Expr::pow(Expr::mul(Expr::Var("a"), Expr::Var("b")), Expr::Const(2.0)); + assert_eq!(format!("{e}"), "(a * b)^2"); +} + +#[test] +fn test_expr_eval_missing_variable() { + // Missing variable should default to 0 + let e = Expr::Var("missing"); + let size = ProblemSize::new(vec![("other", 5)]); + assert_eq!(e.eval(&size), 0.0); +} + +#[test] +fn test_expr_scale() { + let e = Expr::Var("n").scale(3.0); + let size = ProblemSize::new(vec![("n", 5)]); + assert_eq!(e.eval(&size), 15.0); +} + +#[test] +fn test_expr_ops_add_trait() { + let a = Expr::Var("a"); + let b = Expr::Var("b"); + let e = a + b; // uses std::ops::Add + let size = ProblemSize::new(vec![("a", 3), ("b", 4)]); + assert_eq!(e.eval(&size), 7.0); +} + +#[test] +fn test_expr_substitute_exp_log_sqrt() { + let replacement = Expr::Const(2.0); + let mut mapping = HashMap::new(); + mapping.insert("n", &replacement); + + let e = Expr::Exp(Box::new(Expr::Var("n"))); + let result = e.substitute(&mapping); + let size = ProblemSize::new(vec![]); + assert!((result.eval(&size) - 2.0_f64.exp()).abs() < 1e-10); + + let e = Expr::Log(Box::new(Expr::Var("n"))); + let result = e.substitute(&mapping); + assert!((result.eval(&size) - 2.0_f64.ln()).abs() < 1e-10); + + let e = Expr::Sqrt(Box::new(Expr::Var("n"))); + let result = e.substitute(&mapping); + assert!((result.eval(&size) - 2.0_f64.sqrt()).abs() < 1e-10); +} + +#[test] +fn test_expr_variables_exp_log_sqrt() { + let e = Expr::Exp(Box::new(Expr::Var("a"))); + assert_eq!(e.variables(), HashSet::from(["a"])); + + let e = Expr::Log(Box::new(Expr::Var("b"))); + assert_eq!(e.variables(), HashSet::from(["b"])); + + let e = Expr::Sqrt(Box::new(Expr::Var("c"))); + assert_eq!(e.variables(), HashSet::from(["c"])); } diff --git a/src/unit_tests/models/graph/kcoloring.rs b/src/unit_tests/models/graph/kcoloring.rs index 7bfe2fc9a..df23bf8c4 100644 --- a/src/unit_tests/models/graph/kcoloring.rs +++ b/src/unit_tests/models/graph/kcoloring.rs @@ -188,3 +188,10 @@ fn test_is_valid_solution() { // Invalid: adjacent vertices 0 and 1 have same color assert!(!problem.is_valid_solution(&[0, 0, 1])); } + +#[test] +fn test_size_getters() { + let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); + assert_eq!(problem.num_vertices(), 3); + assert_eq!(problem.num_edges(), 2); +} diff --git a/src/unit_tests/models/graph/max_cut.rs b/src/unit_tests/models/graph/max_cut.rs index 32891f08b..6daa8c20f 100644 --- a/src/unit_tests/models/graph/max_cut.rs +++ b/src/unit_tests/models/graph/max_cut.rs @@ -140,3 +140,10 @@ fn test_cut_size_method() { // All same partition: no edges cut assert_eq!(problem.cut_size(&[0, 0, 0]), 0); } + +#[test] +fn test_size_getters() { + let problem = MaxCut::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 2]); + assert_eq!(problem.num_vertices(), 3); + assert_eq!(problem.num_edges(), 2); +} diff --git a/src/unit_tests/models/graph/maximal_is.rs b/src/unit_tests/models/graph/maximal_is.rs index 22069621a..51ef90b09 100644 --- a/src/unit_tests/models/graph/maximal_is.rs +++ b/src/unit_tests/models/graph/maximal_is.rs @@ -173,3 +173,13 @@ fn test_is_valid_solution() { // Invalid: {0} is independent but not maximal (vertex 2 can be added) assert!(!problem.is_valid_solution(&[1, 0, 0])); } + +#[test] +fn test_size_getters() { + let problem = MaximalIS::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + vec![1i32; 4], + ); + assert_eq!(problem.num_vertices(), 4); + assert_eq!(problem.num_edges(), 3); +} diff --git a/src/unit_tests/models/graph/maximum_clique.rs b/src/unit_tests/models/graph/maximum_clique.rs index 9ef849209..4723834ee 100644 --- a/src/unit_tests/models/graph/maximum_clique.rs +++ b/src/unit_tests/models/graph/maximum_clique.rs @@ -284,3 +284,10 @@ fn test_is_valid_solution() { // Invalid: {0, 2} not adjacent assert!(!problem2.is_valid_solution(&[1, 0, 1])); } + +#[test] +fn test_size_getters() { + let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); + assert_eq!(problem.num_vertices(), 3); + assert_eq!(problem.num_edges(), 2); +} diff --git a/src/unit_tests/models/graph/maximum_independent_set.rs b/src/unit_tests/models/graph/maximum_independent_set.rs index 5f3ab2f9e..a4964112a 100644 --- a/src/unit_tests/models/graph/maximum_independent_set.rs +++ b/src/unit_tests/models/graph/maximum_independent_set.rs @@ -172,3 +172,13 @@ fn test_is_valid_solution() { // Invalid: {0, 1} are adjacent assert!(!problem.is_valid_solution(&[1, 1, 0])); } + +#[test] +fn test_size_getters() { + let problem = MaximumIndependentSet::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + vec![1i32; 4], + ); + assert_eq!(problem.num_vertices(), 4); + assert_eq!(problem.num_edges(), 3); +} diff --git a/src/unit_tests/models/graph/maximum_matching.rs b/src/unit_tests/models/graph/maximum_matching.rs index 745cc8050..32068e8f0 100644 --- a/src/unit_tests/models/graph/maximum_matching.rs +++ b/src/unit_tests/models/graph/maximum_matching.rs @@ -166,3 +166,13 @@ fn test_is_valid_solution() { // Invalid: select edges (0,1) and (1,2) — vertex 1 shared assert!(!problem.is_valid_solution(&[1, 1, 0])); } + +#[test] +fn test_size_getters() { + let problem = MaximumMatching::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + vec![1i32; 3], + ); + assert_eq!(problem.num_vertices(), 4); + assert_eq!(problem.num_edges(), 3); +} diff --git a/src/unit_tests/models/graph/minimum_dominating_set.rs b/src/unit_tests/models/graph/minimum_dominating_set.rs index 9945289db..68dd26d2c 100644 --- a/src/unit_tests/models/graph/minimum_dominating_set.rs +++ b/src/unit_tests/models/graph/minimum_dominating_set.rs @@ -169,3 +169,11 @@ fn test_is_valid_solution() { // Invalid: {0} doesn't dominate vertex 2 assert!(!problem.is_valid_solution(&[1, 0, 0])); } + +#[test] +fn test_size_getters() { + let problem = + MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); + assert_eq!(problem.num_vertices(), 3); + assert_eq!(problem.num_edges(), 2); +} diff --git a/src/unit_tests/models/graph/minimum_vertex_cover.rs b/src/unit_tests/models/graph/minimum_vertex_cover.rs index 396105fb5..6934a3574 100644 --- a/src/unit_tests/models/graph/minimum_vertex_cover.rs +++ b/src/unit_tests/models/graph/minimum_vertex_cover.rs @@ -154,3 +154,10 @@ fn test_is_valid_solution() { // Invalid: {0} doesn't cover edge (1,2) assert!(!problem.is_valid_solution(&[1, 0, 0])); } + +#[test] +fn test_size_getters() { + let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); + assert_eq!(problem.num_vertices(), 3); + assert_eq!(problem.num_edges(), 2); +} diff --git a/src/unit_tests/models/graph/traveling_salesman.rs b/src/unit_tests/models/graph/traveling_salesman.rs index 87d471227..f3748d0b6 100644 --- a/src/unit_tests/models/graph/traveling_salesman.rs +++ b/src/unit_tests/models/graph/traveling_salesman.rs @@ -235,3 +235,13 @@ fn test_is_valid_solution() { // Invalid: select only 2 edges — not a cycle assert!(!problem.is_valid_solution(&[1, 1, 0])); } + +#[test] +fn test_size_getters() { + let problem = TravelingSalesman::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + vec![1i32; 3], + ); + assert_eq!(problem.num_vertices(), 3); + assert_eq!(problem.num_edges(), 3); +} diff --git a/src/unit_tests/models/optimization/ilp.rs b/src/unit_tests/models/optimization/ilp.rs index 8bd1968aa..d17c274e3 100644 --- a/src/unit_tests/models/optimization/ilp.rs +++ b/src/unit_tests/models/optimization/ilp.rs @@ -542,3 +542,20 @@ fn test_ilp_problem_minimize() { assert_eq!(Problem::evaluate(&ilp, &[1, 1]), SolutionSize::Valid(2.0)); assert_eq!(ilp.direction(), Direction::Minimize); } + +#[test] +fn test_size_getters() { + let ilp = ILP::new( + 2, + vec![VarBounds::binary(); 2], + vec![ + LinearConstraint::le(vec![(0, 1.0), (1, 1.0)], 3.0), + LinearConstraint::le(vec![(0, 1.0)], 2.0), + ], + vec![(0, 1.0), (1, 2.0)], + ObjectiveSense::Maximize, + ); + assert_eq!(ilp.num_vars(), 2); + assert_eq!(ilp.num_variables(), 2); + assert_eq!(ilp.num_constraints(), 2); +} diff --git a/src/unit_tests/models/optimization/spin_glass.rs b/src/unit_tests/models/optimization/spin_glass.rs index b9432f56d..7018a7473 100644 --- a/src/unit_tests/models/optimization/spin_glass.rs +++ b/src/unit_tests/models/optimization/spin_glass.rs @@ -136,3 +136,14 @@ fn test_jl_parity_evaluation() { assert_eq!(rust_best, jl_best, "SpinGlass best solutions mismatch"); } } + +#[test] +fn test_size_getters() { + let problem = SpinGlass::::new( + 3, + vec![((0, 1), 1.0), ((1, 2), -1.0)], + vec![0.0, 0.0, 0.0], + ); + assert_eq!(problem.num_spins(), 3); + assert_eq!(problem.num_interactions(), 2); +} diff --git a/src/unit_tests/models/satisfiability/ksat.rs b/src/unit_tests/models/satisfiability/ksat.rs index e5c4cf2e2..94f6b306c 100644 --- a/src/unit_tests/models/satisfiability/ksat.rs +++ b/src/unit_tests/models/satisfiability/ksat.rs @@ -212,3 +212,17 @@ fn test_kn_from_k3_clauses() { assert_eq!(k3.evaluate(config), kn.evaluate(config)); } } + +#[test] +fn test_size_getters() { + let problem = KSatisfiability::::new( + 3, + vec![ + CNFClause::new(vec![1, 2, 3]), + CNFClause::new(vec![-1, -2, 3]), + ], + ); + assert_eq!(problem.num_vars(), 3); + assert_eq!(problem.num_clauses(), 2); + assert_eq!(problem.num_literals(), 6); // 3 + 3 +} diff --git a/src/unit_tests/models/set/maximum_set_packing.rs b/src/unit_tests/models/set/maximum_set_packing.rs index 8242c3a9f..7ede648c4 100644 --- a/src/unit_tests/models/set/maximum_set_packing.rs +++ b/src/unit_tests/models/set/maximum_set_packing.rs @@ -146,3 +146,17 @@ fn test_is_valid_solution() { // Invalid: select sets 0 and 1 (share element 1) assert!(!problem.is_valid_solution(&[1, 1, 0])); } + +#[test] +fn test_size_getters() { + // Sets: {0,1}, {2,3}, {4,5} — universe is {0..6} + let problem = MaximumSetPacking::::new(vec![vec![0, 1], vec![2, 3], vec![4, 5]]); + assert_eq!(problem.num_sets(), 3); + assert_eq!(problem.universe_size(), 6); +} + +#[test] +fn test_universe_size_empty() { + let problem = MaximumSetPacking::::new(vec![]); + assert_eq!(problem.universe_size(), 0); +} diff --git a/src/unit_tests/models/specialized/biclique_cover.rs b/src/unit_tests/models/specialized/biclique_cover.rs index bb59df538..d27f0cf89 100644 --- a/src/unit_tests/models/specialized/biclique_cover.rs +++ b/src/unit_tests/models/specialized/biclique_cover.rs @@ -243,3 +243,13 @@ fn test_is_valid_solution() { // Invalid: only left vertex in biclique → doesn't form complete bipartite subgraph covering edge assert!(!problem.is_valid_solution(&[1, 0])); } + +#[test] +fn test_size_getters() { + let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1)]); + let problem = BicliqueCover::new(graph, 1); + assert_eq!(problem.num_vertices(), 4); // 2 left + 2 right + assert_eq!(problem.num_edges(), 2); + assert_eq!(problem.k(), 1); + assert_eq!(problem.rank(), 1); +} diff --git a/src/unit_tests/models/specialized/bmf.rs b/src/unit_tests/models/specialized/bmf.rs index f7e4627e1..6755001b3 100644 --- a/src/unit_tests/models/specialized/bmf.rs +++ b/src/unit_tests/models/specialized/bmf.rs @@ -243,3 +243,13 @@ fn test_jl_parity_evaluation() { assert_eq!(rust_best, jl_best, "BMF best solutions mismatch"); } } + +#[test] +fn test_size_getters() { + let problem = BMF::new( + vec![vec![true, false], vec![false, true], vec![true, true]], + 1, + ); + assert_eq!(problem.m(), 3); // rows + assert_eq!(problem.n(), 2); // cols +} diff --git a/src/unit_tests/models/specialized/circuit.rs b/src/unit_tests/models/specialized/circuit.rs index 6e75834d6..0907980c2 100644 --- a/src/unit_tests/models/specialized/circuit.rs +++ b/src/unit_tests/models/specialized/circuit.rs @@ -250,3 +250,14 @@ fn test_is_valid_solution() { // Invalid: c=1, x=1, y=0 (c = 1 AND 0 = 0, but c=1) assert!(!problem.is_valid_solution(&[1, 1, 0])); } + +#[test] +fn test_size_getters() { + // c = x AND y → variables: c, x, y + let circuit = Circuit::new(vec![Assignment::new( + vec!["c".to_string()], + BooleanExpr::and(vec![BooleanExpr::var("x"), BooleanExpr::var("y")]), + )]); + let problem = CircuitSAT::new(circuit); + assert_eq!(problem.num_variables(), 3); +} diff --git a/src/unit_tests/models/specialized/factoring.rs b/src/unit_tests/models/specialized/factoring.rs index ed6997037..8cfbf47ce 100644 --- a/src/unit_tests/models/specialized/factoring.rs +++ b/src/unit_tests/models/specialized/factoring.rs @@ -106,3 +106,10 @@ fn test_is_valid_solution() { // Invalid: 2 = [0,1,0], 3 = [1,1,0] → 2*3=6 ≠ 15 assert!(!problem.is_valid_solution(&[0, 1, 0, 1, 1, 0])); } + +#[test] +fn test_size_getters() { + let problem = Factoring::new(3, 3, 15); + assert_eq!(problem.num_bits_first(), 3); + assert_eq!(problem.num_bits_second(), 3); +} diff --git a/src/unit_tests/models/specialized/paintshop.rs b/src/unit_tests/models/specialized/paintshop.rs index ce57c2871..b239b7473 100644 --- a/src/unit_tests/models/specialized/paintshop.rs +++ b/src/unit_tests/models/specialized/paintshop.rs @@ -130,3 +130,10 @@ fn test_jl_parity_evaluation() { assert_eq!(rust_best, jl_best, "PaintShop best solutions mismatch"); } } + +#[test] +fn test_size_getters() { + let problem = PaintShop::new(vec!["a", "b", "a", "b"]); + assert_eq!(problem.num_sequence(), 4); + assert_eq!(problem.num_cars(), 2); +}