Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
57ed2b9
update
GiggleLiu Feb 2, 2026
2b769a7
feat: Add problem variants, documentation improvements, and reduction…
GiggleLiu Feb 2, 2026
3e7a953
docs: Add variant trait design document
GiggleLiu Feb 2, 2026
0b2f7a1
docs: Add variant trait implementation plan
GiggleLiu Feb 2, 2026
116c35d
feat: add variant helper module with short_type_name
GiggleLiu Feb 2, 2026
0a9b359
feat: replace GraphType/Weight with variant() in Problem trait
GiggleLiu Feb 2, 2026
0b4ee2d
feat: update graph models to use variant()
GiggleLiu Feb 2, 2026
ab535a6
feat: update satisfiability models to use variant()
GiggleLiu Feb 2, 2026
103fb74
feat: update set models to use variant()
GiggleLiu Feb 2, 2026
b1dcf77
feat: update optimization models to use variant()
GiggleLiu Feb 2, 2026
28184f7
feat: update specialized models to use variant()
GiggleLiu Feb 2, 2026
02785e2
feat: update GraphProblem template to use variant()
GiggleLiu Feb 2, 2026
102688f
feat: update solver test problems to use variant()
GiggleLiu Feb 2, 2026
6174b9d
feat: update ReductionEntry to use variant slices
GiggleLiu Feb 2, 2026
84a3baf
feat: update graph JSON export to structured variant format
GiggleLiu Feb 2, 2026
61f7256
feat: update reduction macro for variant slices
GiggleLiu Feb 2, 2026
db1fbcb
feat: update all reduction registrations to variant format
GiggleLiu Feb 2, 2026
a7cc68a
feat: remove NAME from GraphMarker trait
GiggleLiu Feb 2, 2026
d5c43ce
chore: regenerate reduction graph with new variant format
GiggleLiu Feb 2, 2026
235338c
docs: update reductions.typ for variant() trait design
GiggleLiu Feb 2, 2026
a9fd802
test: add coverage for variant() methods
GiggleLiu Feb 2, 2026
be1c420
test: Add variant() coverage tests for remaining Problem impls
GiggleLiu Feb 2, 2026
7f878ba
test: Add variant() tests for test problems in traits.rs
GiggleLiu Feb 2, 2026
2bf7f49
chore: Add codecov config excluding proc-macro crate
GiggleLiu Feb 2, 2026
afd0c9b
test: Add more is_base_reduction test cases for coverage
GiggleLiu Feb 2, 2026
dda31b5
fix: Update diagram to show edges by base problem names
GiggleLiu Feb 2, 2026
6bf2286
fix: Address PR review comments
GiggleLiu Feb 2, 2026
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
135 changes: 131 additions & 4 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,57 @@ make test clippy export-graph # Must pass before PR
- `src/models/` - Problem implementations (SAT, Graph, Set, Optimization)
- `src/rules/` - Reduction rules + inventory registration
- `src/solvers/` - BruteForce solver, ILP solver (feature-gated)
- `src/traits/` - `Problem`, `ConstraintSatisfactionProblem`, `ReduceTo<T>` traits
- `src/traits.rs` - `Problem`, `ConstraintSatisfactionProblem` traits
- `src/rules/traits.rs` - `ReduceTo<T>`, `ReductionResult` traits
- `src/registry/` - Compile-time reduction metadata collection

### Trait Hierarchy

```
Problem (core trait - all problems must implement)
├── const NAME: &'static str // Problem name, e.g., "IndependentSet"
├── type GraphType: GraphMarker // Graph topology marker
├── type Weight: NumericWeight // Weight type (i32, f64, Unweighted)
├── type Size // Objective value type
├── fn num_variables(&self) -> usize
├── fn num_flavors(&self) -> usize // Usually 2 for binary problems
├── fn problem_size(&self) -> ProblemSize
├── fn energy_mode(&self) -> EnergyMode
├── fn solution_size(&self, config) -> SolutionSize
└── ... (default methods: variables, flavors, is_valid_config)

ConstraintSatisfactionProblem : Problem (extension for CSPs)
├── fn constraints(&self) -> Vec<LocalConstraint>
├── fn objectives(&self) -> Vec<LocalSolutionSize>
├── fn weights(&self) -> Vec<Self::Size>
├── fn set_weights(&mut self, weights)
├── fn is_weighted(&self) -> bool
└── ... (default methods: is_satisfied, compute_objective)
```

### Problem Implementations

| Problem | `Problem` | `ConstraintSatisfactionProblem` |
|---------|:---------:|:-------------------------------:|
| IndependentSet | ✓ | ✓ |
| VertexCovering | ✓ | ✓ |
| DominatingSet | ✓ | ✓ |
| Matching | ✓ | ✓ |
| MaxCut | ✓ | ✗ |
| Coloring | ✓ | ✓ |
| Satisfiability | ✓ | ✓ |
| KSatisfiability | ✓ | ✓ |
| SetPacking | ✓ | ✓ |
| SetCovering | ✓ | ✓ |
| SpinGlass | ✓ | ✗ |
| QUBO | ✓ | ✗ |
| ILP | ✓ | ✗ |
| CircuitSAT | ✓ | ✗ |
| Factoring | ✓ | ✗ |

### Key Patterns
- Problems parameterized by weight type `W` and graph type `G`
- `ReductionResult` provides `target_problem()` and `extract_solution()`
Expand All @@ -38,22 +86,101 @@ make test clippy export-graph # Must pass before PR
- Model files: `src/models/<category>/<name>.rs`
- Test naming: `test_<source>_to_<target>_closed_loop`

### Reduction Pattern
### Reduction Pattern (Recommended: Using Macro)
```rust
impl ReduceTo<TargetProblem> for SourceProblem {
use problemreductions::reduction;

#[reduction(
overhead = { ReductionOverhead::new(vec![...]) }
)]
impl ReduceTo<TargetProblem<Unweighted>> for SourceProblem<Unweighted> {
type Result = ReductionSourceToTarget;
fn reduce_to(&self) -> Self::Result { ... }
}
```

The `#[reduction]` macro automatically:
- Extracts type names from the impl signature
- Detects weighted vs unweighted from type parameters (`Unweighted` vs `i32`/`f64`)
- Detects graph types from type parameters (e.g., `GridGraph`, `SimpleGraph`)
- Generates the `inventory::submit!` call

inventory::submit! { ReductionEntry { source_name, target_name, ... } }
Optional macro attributes:
- `source_graph = "..."` - Override detected source graph type
- `target_graph = "..."` - Override detected target graph type
- `source_weighted = true/false` - Override weighted detection
- `target_weighted = true/false` - Override weighted detection
- `overhead = { ... }` - Specify reduction overhead

### Manual Registration (Alternative)
```rust
inventory::submit! {
ReductionEntry {
source_name: "SourceProblem",
target_name: "TargetProblem",
source_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")],
target_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")],
overhead_fn: || ReductionOverhead::new(...),
}
}
```
Comment on lines 115 to 126
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLAUDE.md documentation shows an outdated manual registration example that uses the old field names:

source_graph: "SimpleGraph",
target_graph: "SimpleGraph",
source_weighted: false,
target_weighted: false,

But the actual ReductionEntry structure now uses:

source_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")],
target_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")],

This example should be updated to match the current API to avoid confusing developers who reference this documentation.

Copilot uses AI. Check for mistakes.

### Weight Types
- `Unweighted` - Marker type for unweighted problems (all weights = 1)
- `i32`, `f64`, etc. - Concrete weight types for weighted problems

### Problem Variant IDs
Reduction graph nodes use variant IDs: `ProblemName[/GraphType][/Weighted]`
- Base: `IndependentSet` (SimpleGraph, unweighted)
- Graph variant: `IndependentSet/GridGraph`
- Weighted variant: `IndependentSet/Weighted`
- Both: `IndependentSet/GridGraph/Weighted`

## Anti-patterns
- Don't create reductions without closed-loop tests
- Don't forget `inventory::submit!` registration (graph won't update)
- Don't hardcode weights - use generic `W` parameter
- Don't skip `make clippy` before PR

## Documentation Requirements

The technical paper (`docs/paper/reductions.typ`) must include:

1. **Table of Contents** - Auto-generated outline of all sections
2. **Problem Data Structures** - For each problem definition, include the Rust struct with fields in a code block
3. **Reduction Examples** - For each reduction theorem, include a minimal working example showing:
- Creating the source problem
- Reducing to target problem
- Solving and extracting solution back
- Based on closed-loop tests from `tests/reduction_tests.rs`

### Documentation Pattern
```typst
#definition("Problem Name")[
Mathematical definition...
]

// Rust data structure
```rust
pub struct ProblemName<W = i32> {
field1: Type1,
field2: Type2,
}
`` `

#theorem[
*(Source → Target)* Reduction description...
]

// Minimal working example
```rust
let source = SourceProblem::new(...);
let reduction = ReduceTo::<TargetProblem>::reduce_to(&source);
let target = reduction.target_problem();
// ... solve and extract
`` `
```

## Contributing
See `.claude/rules/` for detailed guides:
- `adding-reductions.md` - How to add reduction rules
Expand Down
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[workspace]
members = [".", "problemreductions-macros"]

[package]
name = "problemreductions"
version = "0.1.0"
Expand All @@ -23,6 +26,7 @@ good_lp = { version = "1.8", default-features = false, features = ["highs"], opt
inventory = "0.3"
ordered-float = "5.0"
rand = "0.8"
problemreductions-macros = { path = "problemreductions-macros" }

[dev-dependencies]
proptest = "1.0"
Expand Down
22 changes: 22 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Codecov configuration for problem-reductions
# https://docs.codecov.com/docs/codecov-yaml

coverage:
precision: 2
round: down
range: "90...100"

status:
project:
default:
target: 95%
threshold: 2%
patch:
default:
target: 95%
threshold: 2%

# Exclude proc-macro crate from coverage since it runs at compile time
# and traditional runtime coverage tools cannot measure it
ignore:
- "problemreductions-macros/**/*"
157 changes: 157 additions & 0 deletions docs/paper/reduction-diagram.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#import "@preview/fletcher:0.5.8" as fletcher: diagram, node, edge
#import "@preview/cetz:0.4.2": canvas, draw

#let graph-data = json("reduction_graph.json")

#let category-colors = (
"graph": rgb("#e0ffe0"),
"set": rgb("#ffe0e0"),
"optimization": rgb("#ffffd0"),
"satisfiability": rgb("#e0e0ff"),
"specialized": rgb("#ffe0f0"),
"other": rgb("#f0f0f0"),
)

#let get-color(category) = {
category-colors.at(category, default: rgb("#f0f0f0"))
}

// Build node ID from name + variant (new JSON format)
// Format: "Name" for base, "Name/graph/weight" for variants
#let build-node-id(n) = {
if n.variant == (:) or n.variant.keys().len() == 0 {
n.name
} else {
let parts = (n.name,)
if "graph" in n.variant and n.variant.graph != "" and n.variant.graph != "SimpleGraph" {
parts.push(n.variant.graph)
}
if "weight" in n.variant and n.variant.weight != "" and n.variant.weight != "Unweighted" {
parts.push(n.variant.weight)
}
parts.join("/")
}
}

// Build display label from name + variant
#let build-node-label(n) = {
if n.variant == (:) or n.variant.keys().len() == 0 {
n.name
} else {
// For variants, show abbreviated form
let suffix = ()
if "graph" in n.variant and n.variant.graph != "" and n.variant.graph != "SimpleGraph" {
suffix.push(n.variant.graph)
}
// Filter out Unweighted and single-letter generic type params (W, K, T, etc.)
if "weight" in n.variant and n.variant.weight != "" and n.variant.weight != "Unweighted" and n.variant.weight.len() != 1 {
suffix.push(n.variant.weight)
}
if suffix.len() > 0 {
n.name + "/" + suffix.join("/")
} else {
n.name
}
}
}

// Check if node is a base problem (empty variant)
#let is-base-problem(n) = {
n.variant == (:) or n.variant.keys().len() == 0
}

// Base problem positions
#let base-positions = (
// Row 0: Root nodes
"Satisfiability": (-1.5, 0),
"Factoring": (2.5, 0),
// Row 1: Direct children of roots
"KSatisfiability": (-2.5, 1),
"IndependentSet": (-0.5, 1),
"Coloring": (0.5, 1),
"DominatingSet": (-1.5, 1),
"CircuitSAT": (2.5, 1),
// Row 2: Next level
"VertexCovering": (-0.5, 2),
"Matching": (-2, 2),
"SpinGlass": (2.5, 2),
"ILP": (3.5, 1),
// Row 3: Leaf nodes
"SetPacking": (-1.5, 3),
"SetCovering": (0.5, 3),
"MaxCut": (1.5, 3),
"QUBO": (3.5, 3),
"GridGraph": (0.5, 2),
)

// Get position for a node
#let get-node-position(n) = {
if is-base-problem(n) {
// Base problem - use manual position
base-positions.at(n.name, default: (0, 0))
} else {
// Variant - position below parent with horizontal offset
let parent-pos = base-positions.at(n.name, default: (0, 0))
// Find variant index among siblings with same base name
let siblings = graph-data.nodes.filter(x => x.name == n.name and not is-base-problem(x))
let idx = siblings.position(x => build-node-id(x) == build-node-id(n))
let offset = if idx == none { 0 } else { idx * 0.4 }
(parent-pos.at(0) + offset, parent-pos.at(1) + 0.5)
}
}

// Filter to show only base problems in the main diagram
#let base-nodes = graph-data.nodes.filter(n => is-base-problem(n))

// Collect unique base problem names
#let base-names = base-nodes.map(n => n.name)

// Filter edges to only those between base problem names (ignoring variants)
// This allows us to show the high-level structure even though edges connect variant nodes
#let base-edges = graph-data.edges.filter(e => {
base-names.contains(e.source.name) and base-names.contains(e.target.name)
})

// Deduplicate edges by (source-name, target-name) pair, keeping bidirectionality
#let edge-key(e) = if e.source.name < e.target.name {
(e.source.name, e.target.name)
} else {
(e.target.name, e.source.name)
}

// Group edges by their base names and merge bidirectionality
#let edge-map = (:)
#for e in base-edges {
let key = e.source.name + "->" + e.target.name
let rev-key = e.target.name + "->" + e.source.name
if rev-key in edge-map {
// Reverse edge exists, mark as bidirectional
edge-map.at(rev-key).bidirectional = true
} else if key not in edge-map {
edge-map.insert(key, (source: e.source.name, target: e.target.name, bidirectional: e.bidirectional))
}
}

#let deduped-edges = edge-map.values()

#let reduction-graph(width: 18mm, height: 14mm) = diagram(
spacing: (width, height),
node-stroke: 0.6pt,
edge-stroke: 0.6pt,
node-corner-radius: 2pt,
node-inset: 3pt,
..base-nodes.map(n => {
let color = get-color(n.category)
let pos = get-node-position(n)
let node-label = build-node-label(n)
let node-id = build-node-id(n)
node(pos, text(size: 7pt)[#node-label], fill: color, name: label(node-id))
}),
..deduped-edges.map(e => {
let arrow = if e.bidirectional { "<|-|>" } else { "-|>" }
// Use simple name as node ID since we're showing base problems
edge(label(e.source), label(e.target), arrow)
}),
)

#reduction-graph()
Loading