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
2 changes: 1 addition & 1 deletion .claude/skills/add-model/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Create `src/models/<category>/<name>.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;
```
Expand Down
14 changes: 5 additions & 9 deletions .claude/skills/add-rule/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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<TargetType> for SourceType {
type Result = ReductionXToY;
fn reduce_to(&self) -> Self::Result { ... }
Expand Down Expand Up @@ -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!` |
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/review-implementation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/problem.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Connect fields to the symbols defined above.

<!--
Size metrics characterize instance complexity and are used for reduction overhead analysis.
List the named fields returned by `problem_size_names()` / `problem_size_values()`.
List the named getter methods (e.g., num_vertices(), num_edges()) that the problem type provides.
Use symbols defined above.

Examples:
Expand Down
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/rule.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ Solution extraction follows from the variable mapping, no need to describe separ

## Size Overhead

<!-- How large is the target instance as a polynomial of the source size?
<!-- How large is the target instance relative to the source size?
Use the symbols defined in the Reduction Algorithm above.
Also provide the code-level metric name (from the problem's `problem_size()` method). -->
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) |
|----------------------------|----------------------------------|
Expand Down
42 changes: 17 additions & 25 deletions docs/src/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,10 @@ impl<W: WeightElement + VariantParam> ReductionResult for ReductionISToVC<W> {
The `#[reduction]` attribute on the `ReduceTo<T>` 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<MinimumVertexCover<SimpleGraph, i32>>
for MaximumIndependentSet<SimpleGraph, i32>
{
Expand All @@ -212,10 +208,12 @@ inventory::submit! {
target_name: "MinimumVertexCover",
source_variant_fn: || <MaximumIndependentSet<SimpleGraph, i32> as Problem>::variant(),
target_variant_fn: || <MinimumVertexCover<SimpleGraph, i32> 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<dyn DynReductionResult> {
let src = src.downcast_ref::<MaximumIndependentSet<SimpleGraph, i32>>().unwrap();
Expand Down Expand Up @@ -296,23 +294,17 @@ For full type control, you can also chain `ReduceTo::reduce_to()` calls manually
<details>
<summary>Overhead evaluation</summary>

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<Target> 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:

Expand Down
104 changes: 104 additions & 0 deletions src/unit_tests/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]));
}
7 changes: 7 additions & 0 deletions src/unit_tests/models/graph/kcoloring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<K3, _>::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]));
assert_eq!(problem.num_vertices(), 3);
assert_eq!(problem.num_edges(), 2);
}
7 changes: 7 additions & 0 deletions src/unit_tests/models/graph/max_cut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
10 changes: 10 additions & 0 deletions src/unit_tests/models/graph/maximal_is.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
7 changes: 7 additions & 0 deletions src/unit_tests/models/graph/maximum_clique.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
10 changes: 10 additions & 0 deletions src/unit_tests/models/graph/maximum_independent_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
10 changes: 10 additions & 0 deletions src/unit_tests/models/graph/maximum_matching.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
8 changes: 8 additions & 0 deletions src/unit_tests/models/graph/minimum_dominating_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
7 changes: 7 additions & 0 deletions src/unit_tests/models/graph/minimum_vertex_cover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
10 changes: 10 additions & 0 deletions src/unit_tests/models/graph/traveling_salesman.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
17 changes: 17 additions & 0 deletions src/unit_tests/models/optimization/ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Loading