Skip to content

Commit 4cb3bdb

Browse files
authored
Merge pull request #25 from CodingThrust/feat/problem-variants-and-docs
feat: Add problem variants, documentation improvements, and reduction macro
2 parents be40b65 + 6bf2286 commit 4cb3bdb

54 files changed

Lines changed: 3755 additions & 612 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/CLAUDE.md

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,57 @@ make test clippy export-graph # Must pass before PR
2323
- `src/models/` - Problem implementations (SAT, Graph, Set, Optimization)
2424
- `src/rules/` - Reduction rules + inventory registration
2525
- `src/solvers/` - BruteForce solver, ILP solver (feature-gated)
26-
- `src/traits/` - `Problem`, `ConstraintSatisfactionProblem`, `ReduceTo<T>` traits
26+
- `src/traits.rs` - `Problem`, `ConstraintSatisfactionProblem` traits
27+
- `src/rules/traits.rs` - `ReduceTo<T>`, `ReductionResult` traits
2728
- `src/registry/` - Compile-time reduction metadata collection
2829

30+
### Trait Hierarchy
31+
32+
```
33+
Problem (core trait - all problems must implement)
34+
35+
├── const NAME: &'static str // Problem name, e.g., "IndependentSet"
36+
├── type GraphType: GraphMarker // Graph topology marker
37+
├── type Weight: NumericWeight // Weight type (i32, f64, Unweighted)
38+
├── type Size // Objective value type
39+
40+
├── fn num_variables(&self) -> usize
41+
├── fn num_flavors(&self) -> usize // Usually 2 for binary problems
42+
├── fn problem_size(&self) -> ProblemSize
43+
├── fn energy_mode(&self) -> EnergyMode
44+
├── fn solution_size(&self, config) -> SolutionSize
45+
└── ... (default methods: variables, flavors, is_valid_config)
46+
47+
ConstraintSatisfactionProblem : Problem (extension for CSPs)
48+
49+
├── fn constraints(&self) -> Vec<LocalConstraint>
50+
├── fn objectives(&self) -> Vec<LocalSolutionSize>
51+
├── fn weights(&self) -> Vec<Self::Size>
52+
├── fn set_weights(&mut self, weights)
53+
├── fn is_weighted(&self) -> bool
54+
└── ... (default methods: is_satisfied, compute_objective)
55+
```
56+
57+
### Problem Implementations
58+
59+
| Problem | `Problem` | `ConstraintSatisfactionProblem` |
60+
|---------|:---------:|:-------------------------------:|
61+
| IndependentSet |||
62+
| VertexCovering |||
63+
| DominatingSet |||
64+
| Matching |||
65+
| MaxCut |||
66+
| Coloring |||
67+
| Satisfiability |||
68+
| KSatisfiability |||
69+
| SetPacking |||
70+
| SetCovering |||
71+
| SpinGlass |||
72+
| QUBO |||
73+
| ILP |||
74+
| CircuitSAT |||
75+
| Factoring |||
76+
2977
### Key Patterns
3078
- Problems parameterized by weight type `W` and graph type `G`
3179
- `ReductionResult` provides `target_problem()` and `extract_solution()`
@@ -38,22 +86,101 @@ make test clippy export-graph # Must pass before PR
3886
- Model files: `src/models/<category>/<name>.rs`
3987
- Test naming: `test_<source>_to_<target>_closed_loop`
4088

41-
### Reduction Pattern
89+
### Reduction Pattern (Recommended: Using Macro)
4290
```rust
43-
impl ReduceTo<TargetProblem> for SourceProblem {
91+
use problemreductions::reduction;
92+
93+
#[reduction(
94+
overhead = { ReductionOverhead::new(vec![...]) }
95+
)]
96+
impl ReduceTo<TargetProblem<Unweighted>> for SourceProblem<Unweighted> {
4497
type Result = ReductionSourceToTarget;
4598
fn reduce_to(&self) -> Self::Result { ... }
4699
}
100+
```
101+
102+
The `#[reduction]` macro automatically:
103+
- Extracts type names from the impl signature
104+
- Detects weighted vs unweighted from type parameters (`Unweighted` vs `i32`/`f64`)
105+
- Detects graph types from type parameters (e.g., `GridGraph`, `SimpleGraph`)
106+
- Generates the `inventory::submit!` call
47107

48-
inventory::submit! { ReductionEntry { source_name, target_name, ... } }
108+
Optional macro attributes:
109+
- `source_graph = "..."` - Override detected source graph type
110+
- `target_graph = "..."` - Override detected target graph type
111+
- `source_weighted = true/false` - Override weighted detection
112+
- `target_weighted = true/false` - Override weighted detection
113+
- `overhead = { ... }` - Specify reduction overhead
114+
115+
### Manual Registration (Alternative)
116+
```rust
117+
inventory::submit! {
118+
ReductionEntry {
119+
source_name: "SourceProblem",
120+
target_name: "TargetProblem",
121+
source_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")],
122+
target_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")],
123+
overhead_fn: || ReductionOverhead::new(...),
124+
}
125+
}
49126
```
50127

128+
### Weight Types
129+
- `Unweighted` - Marker type for unweighted problems (all weights = 1)
130+
- `i32`, `f64`, etc. - Concrete weight types for weighted problems
131+
132+
### Problem Variant IDs
133+
Reduction graph nodes use variant IDs: `ProblemName[/GraphType][/Weighted]`
134+
- Base: `IndependentSet` (SimpleGraph, unweighted)
135+
- Graph variant: `IndependentSet/GridGraph`
136+
- Weighted variant: `IndependentSet/Weighted`
137+
- Both: `IndependentSet/GridGraph/Weighted`
138+
51139
## Anti-patterns
52140
- Don't create reductions without closed-loop tests
53141
- Don't forget `inventory::submit!` registration (graph won't update)
54142
- Don't hardcode weights - use generic `W` parameter
55143
- Don't skip `make clippy` before PR
56144

145+
## Documentation Requirements
146+
147+
The technical paper (`docs/paper/reductions.typ`) must include:
148+
149+
1. **Table of Contents** - Auto-generated outline of all sections
150+
2. **Problem Data Structures** - For each problem definition, include the Rust struct with fields in a code block
151+
3. **Reduction Examples** - For each reduction theorem, include a minimal working example showing:
152+
- Creating the source problem
153+
- Reducing to target problem
154+
- Solving and extracting solution back
155+
- Based on closed-loop tests from `tests/reduction_tests.rs`
156+
157+
### Documentation Pattern
158+
```typst
159+
#definition("Problem Name")[
160+
Mathematical definition...
161+
]
162+
163+
// Rust data structure
164+
```rust
165+
pub struct ProblemName<W = i32> {
166+
field1: Type1,
167+
field2: Type2,
168+
}
169+
`` `
170+
171+
#theorem[
172+
*(Source → Target)* Reduction description...
173+
]
174+
175+
// Minimal working example
176+
```rust
177+
let source = SourceProblem::new(...);
178+
let reduction = ReduceTo::<TargetProblem>::reduce_to(&source);
179+
let target = reduction.target_problem();
180+
// ... solve and extract
181+
`` `
182+
```
183+
57184
## Contributing
58185
See `.claude/rules/` for detailed guides:
59186
- `adding-reductions.md` - How to add reduction rules

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
[workspace]
2+
members = [".", "problemreductions-macros"]
3+
14
[package]
25
name = "problemreductions"
36
version = "0.1.0"
@@ -23,6 +26,7 @@ good_lp = { version = "1.8", default-features = false, features = ["highs"], opt
2326
inventory = "0.3"
2427
ordered-float = "5.0"
2528
rand = "0.8"
29+
problemreductions-macros = { path = "problemreductions-macros" }
2630

2731
[dev-dependencies]
2832
proptest = "1.0"

codecov.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Codecov configuration for problem-reductions
2+
# https://docs.codecov.com/docs/codecov-yaml
3+
4+
coverage:
5+
precision: 2
6+
round: down
7+
range: "90...100"
8+
9+
status:
10+
project:
11+
default:
12+
target: 95%
13+
threshold: 2%
14+
patch:
15+
default:
16+
target: 95%
17+
threshold: 2%
18+
19+
# Exclude proc-macro crate from coverage since it runs at compile time
20+
# and traditional runtime coverage tools cannot measure it
21+
ignore:
22+
- "problemreductions-macros/**/*"

docs/paper/reduction-diagram.typ

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#import "@preview/fletcher:0.5.8" as fletcher: diagram, node, edge
2+
#import "@preview/cetz:0.4.2": canvas, draw
3+
4+
#let graph-data = json("reduction_graph.json")
5+
6+
#let category-colors = (
7+
"graph": rgb("#e0ffe0"),
8+
"set": rgb("#ffe0e0"),
9+
"optimization": rgb("#ffffd0"),
10+
"satisfiability": rgb("#e0e0ff"),
11+
"specialized": rgb("#ffe0f0"),
12+
"other": rgb("#f0f0f0"),
13+
)
14+
15+
#let get-color(category) = {
16+
category-colors.at(category, default: rgb("#f0f0f0"))
17+
}
18+
19+
// Build node ID from name + variant (new JSON format)
20+
// Format: "Name" for base, "Name/graph/weight" for variants
21+
#let build-node-id(n) = {
22+
if n.variant == (:) or n.variant.keys().len() == 0 {
23+
n.name
24+
} else {
25+
let parts = (n.name,)
26+
if "graph" in n.variant and n.variant.graph != "" and n.variant.graph != "SimpleGraph" {
27+
parts.push(n.variant.graph)
28+
}
29+
if "weight" in n.variant and n.variant.weight != "" and n.variant.weight != "Unweighted" {
30+
parts.push(n.variant.weight)
31+
}
32+
parts.join("/")
33+
}
34+
}
35+
36+
// Build display label from name + variant
37+
#let build-node-label(n) = {
38+
if n.variant == (:) or n.variant.keys().len() == 0 {
39+
n.name
40+
} else {
41+
// For variants, show abbreviated form
42+
let suffix = ()
43+
if "graph" in n.variant and n.variant.graph != "" and n.variant.graph != "SimpleGraph" {
44+
suffix.push(n.variant.graph)
45+
}
46+
// Filter out Unweighted and single-letter generic type params (W, K, T, etc.)
47+
if "weight" in n.variant and n.variant.weight != "" and n.variant.weight != "Unweighted" and n.variant.weight.len() != 1 {
48+
suffix.push(n.variant.weight)
49+
}
50+
if suffix.len() > 0 {
51+
n.name + "/" + suffix.join("/")
52+
} else {
53+
n.name
54+
}
55+
}
56+
}
57+
58+
// Check if node is a base problem (empty variant)
59+
#let is-base-problem(n) = {
60+
n.variant == (:) or n.variant.keys().len() == 0
61+
}
62+
63+
// Base problem positions
64+
#let base-positions = (
65+
// Row 0: Root nodes
66+
"Satisfiability": (-1.5, 0),
67+
"Factoring": (2.5, 0),
68+
// Row 1: Direct children of roots
69+
"KSatisfiability": (-2.5, 1),
70+
"IndependentSet": (-0.5, 1),
71+
"Coloring": (0.5, 1),
72+
"DominatingSet": (-1.5, 1),
73+
"CircuitSAT": (2.5, 1),
74+
// Row 2: Next level
75+
"VertexCovering": (-0.5, 2),
76+
"Matching": (-2, 2),
77+
"SpinGlass": (2.5, 2),
78+
"ILP": (3.5, 1),
79+
// Row 3: Leaf nodes
80+
"SetPacking": (-1.5, 3),
81+
"SetCovering": (0.5, 3),
82+
"MaxCut": (1.5, 3),
83+
"QUBO": (3.5, 3),
84+
"GridGraph": (0.5, 2),
85+
)
86+
87+
// Get position for a node
88+
#let get-node-position(n) = {
89+
if is-base-problem(n) {
90+
// Base problem - use manual position
91+
base-positions.at(n.name, default: (0, 0))
92+
} else {
93+
// Variant - position below parent with horizontal offset
94+
let parent-pos = base-positions.at(n.name, default: (0, 0))
95+
// Find variant index among siblings with same base name
96+
let siblings = graph-data.nodes.filter(x => x.name == n.name and not is-base-problem(x))
97+
let idx = siblings.position(x => build-node-id(x) == build-node-id(n))
98+
let offset = if idx == none { 0 } else { idx * 0.4 }
99+
(parent-pos.at(0) + offset, parent-pos.at(1) + 0.5)
100+
}
101+
}
102+
103+
// Filter to show only base problems in the main diagram
104+
#let base-nodes = graph-data.nodes.filter(n => is-base-problem(n))
105+
106+
// Collect unique base problem names
107+
#let base-names = base-nodes.map(n => n.name)
108+
109+
// Filter edges to only those between base problem names (ignoring variants)
110+
// This allows us to show the high-level structure even though edges connect variant nodes
111+
#let base-edges = graph-data.edges.filter(e => {
112+
base-names.contains(e.source.name) and base-names.contains(e.target.name)
113+
})
114+
115+
// Deduplicate edges by (source-name, target-name) pair, keeping bidirectionality
116+
#let edge-key(e) = if e.source.name < e.target.name {
117+
(e.source.name, e.target.name)
118+
} else {
119+
(e.target.name, e.source.name)
120+
}
121+
122+
// Group edges by their base names and merge bidirectionality
123+
#let edge-map = (:)
124+
#for e in base-edges {
125+
let key = e.source.name + "->" + e.target.name
126+
let rev-key = e.target.name + "->" + e.source.name
127+
if rev-key in edge-map {
128+
// Reverse edge exists, mark as bidirectional
129+
edge-map.at(rev-key).bidirectional = true
130+
} else if key not in edge-map {
131+
edge-map.insert(key, (source: e.source.name, target: e.target.name, bidirectional: e.bidirectional))
132+
}
133+
}
134+
135+
#let deduped-edges = edge-map.values()
136+
137+
#let reduction-graph(width: 18mm, height: 14mm) = diagram(
138+
spacing: (width, height),
139+
node-stroke: 0.6pt,
140+
edge-stroke: 0.6pt,
141+
node-corner-radius: 2pt,
142+
node-inset: 3pt,
143+
..base-nodes.map(n => {
144+
let color = get-color(n.category)
145+
let pos = get-node-position(n)
146+
let node-label = build-node-label(n)
147+
let node-id = build-node-id(n)
148+
node(pos, text(size: 7pt)[#node-label], fill: color, name: label(node-id))
149+
}),
150+
..deduped-edges.map(e => {
151+
let arrow = if e.bidirectional { "<|-|>" } else { "-|>" }
152+
// Use simple name as node ID since we're showing base problems
153+
edge(label(e.source), label(e.target), arrow)
154+
}),
155+
)
156+
157+
#reduction-graph()

0 commit comments

Comments
 (0)