diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 7e267b0..8bbaf87 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -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` traits +- `src/traits.rs` - `Problem`, `ConstraintSatisfactionProblem` traits +- `src/rules/traits.rs` - `ReduceTo`, `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 +├── fn objectives(&self) -> Vec +├── fn weights(&self) -> Vec +├── 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()` @@ -38,22 +86,101 @@ make test clippy export-graph # Must pass before PR - Model files: `src/models//.rs` - Test naming: `test__to__closed_loop` -### Reduction Pattern +### Reduction Pattern (Recommended: Using Macro) ```rust -impl ReduceTo for SourceProblem { +use problemreductions::reduction; + +#[reduction( + overhead = { ReductionOverhead::new(vec![...]) } +)] +impl ReduceTo> for SourceProblem { 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(...), + } +} ``` +### 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 { + field1: Type1, + field2: Type2, +} +`` ` + +#theorem[ + *(Source → Target)* Reduction description... +] + +// Minimal working example +```rust +let source = SourceProblem::new(...); +let reduction = ReduceTo::::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 diff --git a/Cargo.lock b/Cargo.lock index fdf8142..42b46b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -597,6 +597,7 @@ dependencies = [ "num-traits", "ordered-float", "petgraph", + "problemreductions-macros", "proptest", "rand 0.8.5", "serde", @@ -604,6 +605,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "problemreductions-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.105" diff --git a/Cargo.toml b/Cargo.toml index 2ed5bd6..94fa923 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = [".", "problemreductions-macros"] + [package] name = "problemreductions" version = "0.1.0" @@ -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" diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..9e3e35a --- /dev/null +++ b/codecov.yml @@ -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/**/*" diff --git a/docs/paper/reduction-diagram.typ b/docs/paper/reduction-diagram.typ new file mode 100644 index 0000000..e4554f6 --- /dev/null +++ b/docs/paper/reduction-diagram.typ @@ -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() \ No newline at end of file diff --git a/docs/paper/reduction_graph.json b/docs/paper/reduction_graph.json index b9af112..e539e87 100644 --- a/docs/paper/reduction_graph.json +++ b/docs/paper/reduction_graph.json @@ -1,160 +1,454 @@ { "nodes": [ { - "id": "CircuitSAT", - "label": "CircuitSAT", + "name": "CircuitSAT", + "variant": {}, "category": "satisfiability" }, { - "id": "Coloring", - "label": "Coloring", + "name": "CircuitSAT", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + }, + "category": "satisfiability" + }, + { + "name": "CircuitSAT", + "variant": { + "graph": "SimpleGraph", + "weight": "i32" + }, + "category": "satisfiability" + }, + { + "name": "Coloring", + "variant": {}, + "category": "graph" + }, + { + "name": "Coloring", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + }, "category": "graph" }, { - "id": "DominatingSet", - "label": "DominatingSet", + "name": "DominatingSet", + "variant": {}, "category": "graph" }, { - "id": "Factoring", - "label": "Factoring", + "name": "DominatingSet", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + }, + "category": "graph" + }, + { + "name": "Factoring", + "variant": {}, "category": "specialized" }, { - "id": "ILP", - "label": "ILP", + "name": "Factoring", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + }, + "category": "specialized" + }, + { + "name": "ILP", + "variant": {}, + "category": "optimization" + }, + { + "name": "ILP", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + }, "category": "optimization" }, { - "id": "IndependentSet", - "label": "IndependentSet", + "name": "IndependentSet", + "variant": {}, + "category": "graph" + }, + { + "name": "IndependentSet", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + }, "category": "graph" }, { - "id": "KSatisfiability", - "label": "KSatisfiability", + "name": "KSatisfiability", + "variant": {}, "category": "satisfiability" }, { - "id": "Matching", - "label": "Matching", + "name": "KSatisfiability", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + }, + "category": "satisfiability" + }, + { + "name": "Matching", + "variant": {}, + "category": "graph" + }, + { + "name": "Matching", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + }, "category": "graph" }, { - "id": "MaxCut", - "label": "MaxCut", + "name": "MaxCut", + "variant": {}, "category": "graph" }, { - "id": "QUBO", - "label": "QUBO", + "name": "MaxCut", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + }, + "category": "graph" + }, + { + "name": "QUBO", + "variant": {}, "category": "optimization" }, { - "id": "Satisfiability", - "label": "Satisfiability", + "name": "QUBO", + "variant": { + "graph": "SimpleGraph", + "weight": "f64" + }, + "category": "optimization" + }, + { + "name": "Satisfiability", + "variant": {}, "category": "satisfiability" }, { - "id": "SetCovering", - "label": "SetCovering", + "name": "Satisfiability", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + }, + "category": "satisfiability" + }, + { + "name": "SetCovering", + "variant": {}, + "category": "set" + }, + { + "name": "SetCovering", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + }, + "category": "set" + }, + { + "name": "SetPacking", + "variant": {}, "category": "set" }, { - "id": "SetPacking", - "label": "SetPacking", + "name": "SetPacking", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + }, "category": "set" }, { - "id": "SpinGlass", - "label": "SpinGlass", + "name": "SpinGlass", + "variant": {}, "category": "optimization" }, { - "id": "VertexCovering", - "label": "VertexCovering", + "name": "SpinGlass", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + }, + "category": "optimization" + }, + { + "name": "SpinGlass", + "variant": { + "graph": "SimpleGraph", + "weight": "f64" + }, + "category": "optimization" + }, + { + "name": "VertexCovering", + "variant": {}, "category": "graph" }, { - "id": "GridGraph", - "label": "GridGraph", + "name": "VertexCovering", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + }, "category": "graph" } ], "edges": [ { - "source": "CircuitSAT", - "target": "SpinGlass", + "source": { + "name": "CircuitSAT", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, + "target": { + "name": "SpinGlass", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, "bidirectional": false }, { - "source": "Coloring", - "target": "ILP", + "source": { + "name": "Coloring", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, + "target": { + "name": "ILP", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, "bidirectional": false }, { - "source": "Factoring", - "target": "CircuitSAT", + "source": { + "name": "Factoring", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, + "target": { + "name": "CircuitSAT", + "variant": { + "graph": "SimpleGraph", + "weight": "i32" + } + }, "bidirectional": false }, { - "source": "Factoring", - "target": "ILP", + "source": { + "name": "Factoring", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, + "target": { + "name": "ILP", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, "bidirectional": false }, { - "source": "KSatisfiability", - "target": "Satisfiability", - "bidirectional": true - }, - { - "source": "Matching", - "target": "SetPacking", + "source": { + "name": "Matching", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, + "target": { + "name": "SetPacking", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, "bidirectional": false }, { - "source": "Satisfiability", - "target": "Coloring", + "source": { + "name": "Satisfiability", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, + "target": { + "name": "Coloring", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, "bidirectional": false }, { - "source": "Satisfiability", - "target": "DominatingSet", + "source": { + "name": "Satisfiability", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, + "target": { + "name": "DominatingSet", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, "bidirectional": false }, { - "source": "Satisfiability", - "target": "IndependentSet", + "source": { + "name": "Satisfiability", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, + "target": { + "name": "IndependentSet", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, "bidirectional": false }, { - "source": "SetPacking", - "target": "IndependentSet", + "source": { + "name": "Satisfiability", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, + "target": { + "name": "KSatisfiability", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, "bidirectional": true }, { - "source": "SpinGlass", - "target": "MaxCut", + "source": { + "name": "SetPacking", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, + "target": { + "name": "IndependentSet", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, "bidirectional": true }, { - "source": "SpinGlass", - "target": "QUBO", + "source": { + "name": "SpinGlass", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, + "target": { + "name": "MaxCut", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, "bidirectional": true }, { - "source": "VertexCovering", - "target": "IndependentSet", + "source": { + "name": "SpinGlass", + "variant": { + "graph": "SimpleGraph", + "weight": "f64" + } + }, + "target": { + "name": "QUBO", + "variant": { + "graph": "SimpleGraph", + "weight": "f64" + } + }, "bidirectional": true }, { - "source": "VertexCovering", - "target": "SetCovering", - "bidirectional": false + "source": { + "name": "VertexCovering", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, + "target": { + "name": "IndependentSet", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, + "bidirectional": true }, { - "source": "IndependentSet", - "target": "GridGraph", + "source": { + "name": "VertexCovering", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, + "target": { + "name": "SetCovering", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, "bidirectional": false } ] diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index a11eddf..b8e468f 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -1,77 +1,30 @@ // Problem Reductions: A Mathematical Reference - -#import "@preview/fletcher:0.5.8" as fletcher: diagram, node, edge -#import "@preview/cetz:0.4.0": canvas, draw +#import "reduction-diagram.typ": reduction-graph, graph-data +#import "@preview/cetz:0.4.2": canvas, draw +#import "@preview/ctheorems:1.1.3": thmbox, thmplain, thmproof, thmrules #set page(paper: "a4", margin: (x: 2cm, y: 2.5cm)) #set text(font: "New Computer Modern", size: 10pt) #set par(justify: true) #set heading(numbering: "1.1") -#let theorem-counter = counter("theorem") +#show link: set text(blue) -#let theorem(body) = block( - width: 100%, - inset: (x: 0em, y: 0.5em), - { - theorem-counter.step() - [*Theorem #context theorem-counter.display().* ] - body - } -) +// Table of contents +#outline(title: "Contents", indent: 1.5em, depth: 2) -#let proof(body) = block( - width: 100%, - inset: (x: 0em, y: 0.3em), - [_Proof._ #body #h(1fr) $square$] -) +// Set up theorem environments with ctheorems +#show: thmrules.with(qed-symbol: $square$) -#let definition(title, body) = block( - width: 100%, - inset: (x: 1em, y: 0.8em), +#let theorem = thmplain("theorem", "Theorem").with(numbering: none) +#let proof = thmproof("proof", "Proof") +#let definition = thmbox( + "definition", + "Definition", fill: rgb("#f8f8f8"), stroke: (left: 2pt + rgb("#4a86e8")), - [*#title.* #body] -) - -#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")) -} - -// Optimized layout: SAT branch (left) + Physics branch (right) -// Node IDs use base names without type parameters -#let node-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), + inset: (x: 1em, y: 0.8em), + base_level: 1, ) #align(center)[ @@ -98,27 +51,7 @@ A _reduction_ from problem $A$ to problem $B$, denoted $A arrow.long B$, is a po We use the following notation throughout. An _undirected graph_ $G = (V, E)$ consists of a vertex set $V$ and edge set $E subset.eq binom(V, 2)$. For a set $S$, $overline(S)$ or $V backslash S$ denotes its complement. We write $|S|$ for cardinality. For Boolean variables, $overline(x)$ denotes negation ($not x$). A _literal_ is a variable $x$ or its negation $overline(x)$. A _clause_ is a disjunction of literals. A formula in _conjunctive normal form_ (CNF) is a conjunction of clauses. We abbreviate Independent Set as IS, Vertex Cover as VC, and use $n$ for problem size, $m$ for number of clauses, and $k_j = |C_j|$ for clause size. #figure( - box( - width: 70%, - align(center, - diagram( - spacing: (18mm, 14mm), - node-stroke: 0.6pt, - edge-stroke: 0.6pt, - node-corner-radius: 2pt, - node-inset: 3pt, - ..graph-data.nodes.map(n => { - let color = get-color(n.category) - let pos = node-positions.at(n.id, default: (0, 0)) - node(pos, text(size: 7pt)[#n.label], fill: color, name: label(n.id)) - }), - ..graph-data.edges.map(e => { - let arrow = if e.bidirectional { "<|-|>" } else { "-|>" } - edge(label(e.source), label(e.target), arrow) - }), - ) - ) - ), + reduction-graph(width: 18mm, height: 14mm), caption: [Reduction graph. Colors: green (graph), red (set), yellow (optimization), blue (satisfiability), pink (specialized).] ) @@ -130,27 +63,137 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #definition("Independent Set (IS)")[ Given $G = (V, E)$ with vertex weights $w: V -> RR$, find $S subset.eq V$ maximizing $sum_(v in S) w(v)$ such that no two vertices in $S$ are adjacent: $forall u, v in S: (u, v) in.not E$. -] + + _Reduces to:_ Set Packing (@def:set-packing). + + _Reduces from:_ Vertex Cover (@def:vertex-cover), SAT (@def:satisfiability), Set Packing (@def:set-packing). + + ```rust + pub struct IndependentSet { + graph: UnGraph<(), ()>, // The underlying graph + weights: Vec, // Weights for each vertex + } + + impl Problem for IndependentSet { + const NAME: &'static str = "IndependentSet"; + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", short_type_name::())] + } + // ... + } + ``` +] #definition("Vertex Cover (VC)")[ Given $G = (V, E)$ with vertex weights $w: V -> RR$, find $S subset.eq V$ minimizing $sum_(v in S) w(v)$ such that every edge has at least one endpoint in $S$: $forall (u, v) in E: u in S or v in S$. -] + + _Reduces to:_ Independent Set (@def:independent-set), Set Covering (@def:set-covering). + + _Reduces from:_ Independent Set (@def:independent-set). + + ```rust + pub struct VertexCovering { + graph: UnGraph<(), ()>, // The underlying graph + weights: Vec, // Weights for each vertex + } + + impl Problem for VertexCovering { + const NAME: &'static str = "VertexCovering"; + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", short_type_name::())] + } + // ... + } + ``` +] #definition("Max-Cut")[ Given $G = (V, E)$ with weights $w: E -> RR$, find partition $(S, overline(S))$ maximizing $sum_((u,v) in E: u in S, v in overline(S)) w(u, v)$. -] + + _Reduces to:_ Spin Glass (@def:spin-glass). + + _Reduces from:_ Spin Glass (@def:spin-glass). + + ```rust + pub struct MaxCut { + graph: UnGraph<(), W>, // Weighted graph (edge weights) + } + + impl Problem for MaxCut { + const NAME: &'static str = "MaxCut"; + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", short_type_name::())] + } + // ... + } + ``` +] #definition("Graph Coloring")[ Given $G = (V, E)$ and $k$ colors, find $c: V -> {1, ..., k}$ minimizing $|{(u, v) in E : c(u) = c(v)}|$. -] + + _Reduces to:_ ILP (@def:ilp). + + _Reduces from:_ SAT (@def:satisfiability). + + ```rust + pub struct Coloring { + num_colors: usize, // Number of available colors (K) + graph: UnGraph<(), ()>, // The underlying graph + } + + impl Problem for Coloring { + const NAME: &'static str = "Coloring"; + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "Unweighted")] + } + // ... + } + ``` +] #definition("Dominating Set")[ Given $G = (V, E)$ with weights $w: V -> RR$, find $S subset.eq V$ minimizing $sum_(v in S) w(v)$ s.t. $forall v in V: v in S or exists u in S: (u, v) in E$. -] + + _Reduces from:_ SAT (@def:satisfiability). + + ```rust + pub struct DominatingSet { + graph: UnGraph<(), ()>, // The underlying graph + weights: Vec, // Weights for each vertex + } + + impl Problem for DominatingSet { + const NAME: &'static str = "DominatingSet"; + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", short_type_name::())] + } + // ... + } + ``` +] #definition("Matching")[ Given $G = (V, E)$ with weights $w: E -> RR$, find $M subset.eq E$ maximizing $sum_(e in M) w(e)$ s.t. $forall e_1, e_2 in M: e_1 inter e_2 = emptyset$. -] + + _Reduces to:_ Set Packing (@def:set-packing). + + ```rust + pub struct Matching { + num_vertices: usize, // Number of vertices + graph: UnGraph<(), W>, // Weighted graph + edge_weights: Vec, // Weights for each edge + } + + impl Problem for Matching { + const NAME: &'static str = "Matching"; + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", short_type_name::())] + } + // ... + } + ``` +] #definition("Unit Disk Graph (Grid Graph)")[ A graph $G = (V, E)$ where vertices $V$ are points on a 2D lattice and $(u, v) in E$ iff the Euclidean distance $d(u, v) <= r$ for some radius $r$. A _King's subgraph_ uses the King's graph lattice (8-connectivity square grid) with $r approx 1.5$. @@ -160,84 +203,309 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #definition("Set Packing")[ Given universe $U$, collection $cal(S) = {S_1, ..., S_m}$ with $S_i subset.eq U$, weights $w: cal(S) -> RR$, find $cal(P) subset.eq cal(S)$ maximizing $sum_(S in cal(P)) w(S)$ s.t. $forall S_i, S_j in cal(P): S_i inter S_j = emptyset$. -] + + _Reduces to:_ Independent Set (@def:independent-set). + + _Reduces from:_ Independent Set (@def:independent-set), Matching (@def:matching). + + ```rust + pub struct SetPacking { + sets: Vec>, // Collection of sets + weights: Vec, // Weights for each set + } + + impl Problem for SetPacking { + const NAME: &'static str = "SetPacking"; + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", short_type_name::())] + } + // ... + } + ``` +] #definition("Set Covering")[ Given universe $U$, collection $cal(S)$ with weights $w: cal(S) -> RR$, find $cal(C) subset.eq cal(S)$ minimizing $sum_(S in cal(C)) w(S)$ s.t. $union.big_(S in cal(C)) S = U$. -] + + _Reduces from:_ Vertex Cover (@def:vertex-cover). + + ```rust + pub struct SetCovering { + universe_size: usize, // Size of the universe + sets: Vec>, // Collection of sets + weights: Vec, // Weights for each set + } + + impl Problem for SetCovering { + const NAME: &'static str = "SetCovering"; + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", short_type_name::())] + } + // ... + } + ``` +] == Optimization Problems #definition("Spin Glass (Ising Model)")[ Given $n$ spin variables $s_i in {-1, +1}$, pairwise couplings $J_(i j) in RR$, and external fields $h_i in RR$, minimize the Hamiltonian (energy function): $H(bold(s)) = -sum_((i,j)) J_(i j) s_i s_j - sum_i h_i s_i$. -] + + _Reduces to:_ Max-Cut (@def:max-cut), QUBO (@def:qubo). + + _Reduces from:_ Circuit-SAT (@def:circuit-sat), Max-Cut (@def:max-cut), QUBO (@def:qubo). + + ```rust + pub struct SpinGlass { + num_spins: usize, // Number of spins + interactions: Vec<((usize, usize), W)>, // J_ij couplings + fields: Vec, // h_i on-site fields + } + + impl Problem for SpinGlass { + const NAME: &'static str = "SpinGlass"; + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", short_type_name::())] + } + // ... + } + ``` +] #definition("QUBO")[ Given $n$ binary variables $x_i in {0, 1}$, matrix $Q in RR^(n times n)$, minimize $f(bold(x)) = bold(x)^top Q bold(x)$. -] + + _Reduces to:_ Spin Glass (@def:spin-glass). + + _Reduces from:_ Spin Glass (@def:spin-glass). + + ```rust + pub struct QUBO { + num_vars: usize, // Number of variables + matrix: Vec>, // Q matrix (upper triangular) + } + + impl Problem for QUBO { + const NAME: &'static str = "QUBO"; + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", short_type_name::())] + } + // ... + } + ``` +] #definition("Integer Linear Programming (ILP)")[ Given $n$ integer variables $bold(x) in ZZ^n$, constraint matrix $A in RR^(m times n)$, bounds $bold(b) in RR^m$, and objective $bold(c) in RR^n$, find $bold(x)$ minimizing $bold(c)^top bold(x)$ subject to $A bold(x) <= bold(b)$ and variable bounds. -] + + _Reduces from:_ Graph Coloring (@def:coloring), Factoring (@def:factoring). + + ```rust + pub struct ILP { + num_vars: usize, // Number of variables + bounds: Vec, // Bounds per variable + constraints: Vec, // Linear constraints + objective: Vec<(usize, f64)>, // Sparse objective + sense: ObjectiveSense, // Maximize or Minimize + } + + pub struct VarBounds { lower: Option, upper: Option } + pub struct LinearConstraint { + terms: Vec<(usize, f64)>, // (var_index, coefficient) + cmp: Comparison, // Le, Ge, or Eq + rhs: f64, + } + + impl Problem for ILP { + const NAME: &'static str = "ILP"; + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "f64")] + } + // ... + } + ``` +] == Satisfiability Problems #definition("SAT")[ Given a CNF formula $phi = and.big_(j=1)^m C_j$ with $m$ clauses over $n$ Boolean variables, where each clause $C_j = or.big_i ell_(j i)$ is a disjunction of literals, find an assignment $bold(x) in {0, 1}^n$ such that $phi(bold(x)) = 1$ (all clauses satisfied). -] -#definition("$k$-SAT")[ + _Reduces to:_ Independent Set (@def:independent-set), Graph Coloring (@def:coloring), Dominating Set (@def:dominating-set), $k$-SAT (@def:k-sat). + + _Reduces from:_ $k$-SAT (@def:k-sat). + + ```rust + pub struct Satisfiability { + num_vars: usize, // Number of variables + clauses: Vec, // Clauses in CNF + weights: Vec, // Weights per clause (MAX-SAT) + } + + pub struct CNFClause { + literals: Vec, // Signed: +i for x_i, -i for NOT x_i + } + + impl Problem for Satisfiability { + const NAME: &'static str = "Satisfiability"; + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", short_type_name::())] + } + // ... + } + ``` +] + +#definition([$k$-SAT])[ SAT with exactly $k$ literals per clause. -] + + _Reduces to:_ SAT (@def:satisfiability). + + _Reduces from:_ SAT (@def:satisfiability). + + ```rust + pub struct KSatisfiability { + num_vars: usize, // Number of variables + clauses: Vec, // Each clause has exactly K literals + weights: Vec, // Weights per clause + } + + impl Problem for KSatisfiability { + const NAME: &'static str = "KSatisfiability"; + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", short_type_name::())] + } + // ... + } + ``` +] #definition("Circuit-SAT")[ Given a Boolean circuit $C$ composed of logic gates (AND, OR, NOT, XOR) with $n$ input variables, find an input assignment $bold(x) in {0,1}^n$ such that $C(bold(x)) = 1$. -] + + _Reduces to:_ Spin Glass (@def:spin-glass). + + _Reduces from:_ Factoring (@def:factoring). + + ```rust + pub struct CircuitSAT { + circuit: Circuit, // The boolean circuit + variables: Vec, // Variable names in order + weights: Vec, // Weights per assignment + } + + pub struct Circuit { assignments: Vec } + pub struct Assignment { outputs: Vec, expr: BooleanExpr } + pub enum BooleanOp { Var(String), Const(bool), Not(..), And(..), Or(..), Xor(..) } + + impl Problem for CircuitSAT { + const NAME: &'static str = "CircuitSAT"; + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", short_type_name::())] + } + // ... + } + ``` +] #definition("Factoring")[ Given a composite integer $N$ and bit sizes $m, n$, find integers $p in [2, 2^m - 1]$ and $q in [2, 2^n - 1]$ such that $p times q = N$. Here $p$ has $m$ bits and $q$ has $n$ bits. -] + + _Reduces to:_ Circuit-SAT (@def:circuit-sat), ILP (@def:ilp). + + ```rust + pub struct Factoring { + m: usize, // Bits for first factor + n: usize, // Bits for second factor + target: u64, // The number to factor + } + + impl Problem for Factoring { + const NAME: &'static str = "Factoring"; + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "i32")] + } + // ... + } + ``` +] = Reductions == Trivial Reductions #theorem[ - *(IS $arrow.l.r$ VC)* $S subset.eq V$ is independent iff $V backslash S$ is a vertex cover, with $|"IS"| + |"VC"| = |V|$. + *(IS $arrow.l.r$ VC)* $S subset.eq V$ is independent iff $V backslash S$ is a vertex cover, with $|"IS"| + |"VC"| = |V|$. [_Problems:_ @def:independent-set, @def:vertex-cover.] ] #proof[ ($arrow.r.double$) If $S$ is independent, for any $(u, v) in E$, at most one endpoint lies in $S$, so $V backslash S$ covers all edges. ($arrow.l.double$) If $C$ is a cover, for any $u, v in V backslash C$, $(u, v) in.not E$, so $V backslash C$ is independent. ] +```rust +// Minimal example: IS -> VC -> extract solution +let is_problem = IndependentSet::::new(3, vec![(0, 1), (1, 2), (0, 2)]); +let result = ReduceTo::>::reduce_to(&is_problem); +let vc_problem = result.target_problem(); + +let solver = BruteForce::new(); +let vc_solutions = solver.find_best(vc_problem); +let is_solution = result.extract_solution(&vc_solutions[0]); +assert!(is_problem.solution_size(&is_solution).is_valid); +``` + #theorem[ - *(IS $arrow.r$ Set Packing)* Construct $U = E$, $S_v = {e in E : v in e}$, $w(S_v) = w(v)$. Then $I$ is independent iff ${S_v : v in I}$ is a packing. + *(IS $arrow.r$ Set Packing)* Construct $U = E$, $S_v = {e in E : v in e}$, $w(S_v) = w(v)$. Then $I$ is independent iff ${S_v : v in I}$ is a packing. [_Problems:_ @def:independent-set, @def:set-packing.] ] #proof[ Independence implies disjoint incident edge sets; conversely, disjoint edge sets imply no shared edges. ] +```rust +// Minimal example: IS -> SetPacking -> extract solution +let is_problem = IndependentSet::::new(3, vec![(0, 1), (1, 2), (0, 2)]); +let result = ReduceTo::>::reduce_to(&is_problem); +let sp_problem = result.target_problem(); + +let solver = BruteForce::new(); +let sp_solutions = solver.find_best(sp_problem); +let is_solution = result.extract_solution(&sp_solutions[0]); +assert!(is_problem.solution_size(&is_solution).is_valid); +``` + #theorem[ - *(VC $arrow.r$ Set Covering)* Construct $U = {0, ..., |E|-1}$, $S_v = {i : e_i "incident to" v}$, $w(S_v) = w(v)$. Then $C$ is a cover iff ${S_v : v in C}$ covers $U$. + *(VC $arrow.r$ Set Covering)* Construct $U = {0, ..., |E|-1}$, $S_v = {i : e_i "incident to" v}$, $w(S_v) = w(v)$. Then $C$ is a cover iff ${S_v : v in C}$ covers $U$. [_Problems:_ @def:vertex-cover, @def:set-covering.] ] #theorem[ - *(Matching $arrow.r$ Set Packing)* Construct $U = V$, $S_e = {u, v}$ for $e = (u,v)$, $w(S_e) = w(e)$. Then $M$ is a matching iff ${S_e : e in M}$ is a packing. + *(Matching $arrow.r$ Set Packing)* Construct $U = V$, $S_e = {u, v}$ for $e = (u,v)$, $w(S_e) = w(e)$. Then $M$ is a matching iff ${S_e : e in M}$ is a packing. [_Problems:_ @def:matching, @def:set-packing.] ] #theorem[ - *(Spin Glass $arrow.l.r$ QUBO)* The substitution $s_i = 2x_i - 1$ yields $H_"SG"(bold(s)) = H_"QUBO"(bold(x)) + "const"$. + *(Spin Glass $arrow.l.r$ QUBO)* The substitution $s_i = 2x_i - 1$ yields $H_"SG"(bold(s)) = H_"QUBO"(bold(x)) + "const"$. [_Problems:_ @def:spin-glass, @def:qubo.] ] #proof[ Expanding $-sum_(i,j) J_(i j) (2x_i - 1)(2x_j - 1) - sum_i h_i (2x_i - 1)$ gives $Q_(i j) = -4J_(i j)$, $Q_(i i) = 2sum_j J_(i j) - 2h_i$. ] +```rust +// Minimal example: SpinGlass -> QUBO -> extract solution +let sg = SpinGlass::new(2, vec![((0, 1), -1.0)], vec![0.5, -0.5]); +let result = ReduceTo::::reduce_to(&sg); +let qubo = result.target_problem(); + +let solver = BruteForce::new(); +let qubo_solutions = solver.find_best(qubo); +let sg_solution = result.extract_solution(&qubo_solutions[0]); +assert_eq!(sg_solution.len(), 2); +``` + == Non-Trivial Reductions #theorem[ - *(SAT $arrow.r$ IS)* @karp1972 Given CNF $phi$ with $m$ clauses, construct graph $G$ such that $phi$ is satisfiable iff $G$ has an IS of size $m$. + *(SAT $arrow.r$ IS)* @karp1972 Given CNF $phi$ with $m$ clauses, construct graph $G$ such that $phi$ is satisfiable iff $G$ has an IS of size $m$. [_Problems:_ @def:satisfiability, @def:independent-set.] ] #proof[ @@ -253,7 +521,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] #theorem[ - *(SAT $arrow.r$ 3-Coloring)* @garey1979 Given CNF $phi$, construct graph $G$ such that $phi$ is satisfiable iff $G$ is 3-colorable. + *(SAT $arrow.r$ 3-Coloring)* @garey1979 Given CNF $phi$, construct graph $G$ such that $phi$ is satisfiable iff $G$ is 3-colorable. [_Problems:_ @def:satisfiability, @def:coloring.] ] #proof[ @@ -265,7 +533,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] #theorem[ - *(SAT $arrow.r$ Dominating Set)* @garey1979 Given CNF $phi$ with $n$ variables and $m$ clauses, $phi$ is satisfiable iff the constructed graph has a dominating set of size $n$. + *(SAT $arrow.r$ Dominating Set)* @garey1979 Given CNF $phi$ with $n$ variables and $m$ clauses, $phi$ is satisfiable iff the constructed graph has a dominating set of size $n$. [_Problems:_ @def:satisfiability, @def:dominating-set.] ] #proof[ @@ -277,7 +545,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] #theorem[ - *(SAT $arrow.l.r$ $k$-SAT)* @cook1971 @garey1979 Any SAT formula converts to $k$-SAT ($k >= 3$) preserving satisfiability. + *(SAT $arrow.l.r$ $k$-SAT)* @cook1971 @garey1979 Any SAT formula converts to $k$-SAT ($k >= 3$) preserving satisfiability. [_Problems:_ @def:satisfiability, @def:k-sat.] ] #proof[ @@ -290,7 +558,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] #theorem[ - *(CircuitSAT $arrow.r$ Spin Glass)* @whitfield2012 @lucas2014 Each gate maps to a gadget whose ground states encode valid I/O. + *(CircuitSAT $arrow.r$ Spin Glass)* @whitfield2012 @lucas2014 Each gate maps to a gadget whose ground states encode valid I/O. [_Problems:_ @def:circuit-sat, @def:spin-glass.] ] #proof[ @@ -314,7 +582,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ) #theorem[ - *(Factoring $arrow.r$ Circuit-SAT)* An array multiplier with output constrained to $N$ is satisfiable iff $N$ factors within bit bounds. _(Folklore; no canonical reference.)_ + *(Factoring $arrow.r$ Circuit-SAT)* An array multiplier with output constrained to $N$ is satisfiable iff $N$ factors within bit bounds. _(Folklore; no canonical reference.)_ [_Problems:_ @def:factoring, @def:circuit-sat.] ] #proof[ @@ -330,7 +598,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] #theorem[ - *(Spin Glass $arrow.l.r$ Max-Cut)* @barahona1982 @lucas2014 Ground states of Ising models correspond to maximum cuts. + *(Spin Glass $arrow.l.r$ Max-Cut)* @barahona1982 @lucas2014 Ground states of Ising models correspond to maximum cuts. [_Problems:_ @def:spin-glass, @def:max-cut.] ] #proof[ @@ -341,8 +609,20 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| _Solution extraction._ Without ancilla: identity. With ancilla: if $sigma_a = 1$, flip all spins before removing ancilla. ] +```rust +// Minimal example: SpinGlass -> MaxCut -> extract solution +let sg = SpinGlass::new(3, vec![((0, 1), 1), ((1, 2), 1), ((0, 2), 1)], vec![0, 0, 0]); +let result = ReduceTo::>::reduce_to(&sg); +let maxcut = result.target_problem(); + +let solver = BruteForce::new(); +let maxcut_solutions = solver.find_best(maxcut); +let sg_solution = result.extract_solution(&maxcut_solutions[0]); +assert_eq!(sg_solution.len(), 3); +``` + #theorem[ - *(Coloring $arrow.r$ ILP)* The $k$-coloring problem reduces to binary ILP with $|V| dot k$ variables and $|V| + |E| dot k$ constraints. + *(Coloring $arrow.r$ ILP)* The $k$-coloring problem reduces to binary ILP with $|V| dot k$ variables and $|V| + |E| dot k$ constraints. [_Problems:_ @def:coloring, @def:ilp.] ] #proof[ @@ -360,7 +640,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] #theorem[ - *(Factoring $arrow.r$ ILP)* Integer factorization reduces to binary ILP using McCormick linearization with $O(m n)$ variables and constraints. + *(Factoring $arrow.r$ ILP)* Integer factorization reduces to binary ILP using McCormick linearization with $O(m n)$ variables and constraints. [_Problems:_ @def:factoring, @def:ilp.] ] #proof[ @@ -409,7 +689,7 @@ assert_eq!(p * q, 15); // e.g., (3, 5) or (5, 3) == Unit Disk Mapping #theorem[ - *(IS $arrow.r$ GridGraph IS)* @nguyen2023 Any MIS problem on a general graph $G$ can be reduced to MIS on a unit disk graph (King's subgraph) with at most quadratic overhead in the number of vertices. + *(IS $arrow.r$ GridGraph IS)* @nguyen2023 Any MIS problem on a general graph $G$ can be reduced to MIS on a unit disk graph (King's subgraph) with at most quadratic overhead in the number of vertices. [_Problem:_ @def:independent-set.] ] #proof[ diff --git a/docs/plans/2026-02-02-problem-variants-design.md b/docs/plans/2026-02-02-problem-variants-design.md new file mode 100644 index 0000000..1aaeae9 --- /dev/null +++ b/docs/plans/2026-02-02-problem-variants-design.md @@ -0,0 +1,99 @@ +# Problem Variants in Reduction Diagram + +**Date:** 2026-02-02 +**Status:** Approved + +## Overview + +Show problem variants (different graph types, weighted/unweighted) as separate nodes in the reduction diagram. Variants are positioned directly below their parent problem. + +## Naming Convention + +- Base problem (SimpleGraph + Unweighted): `IndependentSet` +- Graph variant only: `IndependentSet/GridGraph` +- Weight variant only: `IndependentSet/Weighted` +- Both variants: `IndependentSet/GridGraph/Weighted` + +Default types (SimpleGraph, Unweighted) are omitted from the ID. + +## The `Unweighted` Marker Type + +```rust +/// Marker type for unweighted problems. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Unweighted; +``` + +Problems explicitly typed as: +- `IndependentSet` - unweighted (default) +- `IndependentSet` - weighted with integer weights + +## ReductionEntry Changes + +```rust +pub struct ReductionEntry { + pub source_name: &'static str, + pub target_name: &'static str, + pub source_graph: &'static str, + pub target_graph: &'static str, + pub source_weighted: bool, // NEW + pub target_weighted: bool, // NEW + pub overhead_fn: fn() -> ReductionOverhead, +} +``` + +Helper method generates variant IDs: +```rust +fn variant_id(name: &str, graph: &str, weighted: bool) -> String { + let mut id = name.to_string(); + if graph != "SimpleGraph" && graph != "CNF" && graph != "SetSystem" { + id.push('/'); + id.push_str(graph); + } + if weighted { + id.push_str("/Weighted"); + } + id +} +``` + +## JSON Schema Extension + +```json +{ + "nodes": [ + {"id": "IndependentSet", "label": "IndependentSet", "category": "graph", + "parent": null, "graph_type": "SimpleGraph", "weighted": false}, + {"id": "IndependentSet/GridGraph/Weighted", "label": "GridGraph/Weighted", + "category": "graph", "parent": "IndependentSet", + "graph_type": "GridGraph", "weighted": true} + ] +} +``` + +## Diagram Layout + +Variants positioned 0.5 units below parent, offset horizontally if multiple: +``` +IndependentSet ←→ VertexCovering + │ + GridGraph/ + Weighted +``` + +## Implementation Order + +1. Add `Unweighted` type in `src/types.rs` +2. Extend `ReductionEntry` in `src/rules/registry.rs` +3. Update all reduction registrations (~12 files) +4. Update graph generation in `src/rules/graph.rs` +5. Update Typst diagram in `docs/paper/reduction-diagram.typ` +6. Regenerate with `make export-graph && make paper` + +## Files to Modify + +- `src/types.rs` - Add `Unweighted` struct +- `src/rules/registry.rs` - Extend `ReductionEntry` +- `src/rules/graph.rs` - Update JSON generation +- `src/rules/*.rs` - All reduction files (~12) +- `docs/paper/reduction-diagram.typ` - Auto-position variants diff --git a/docs/plans/2026-02-02-variant-trait-design.md b/docs/plans/2026-02-02-variant-trait-design.md new file mode 100644 index 0000000..6091157 --- /dev/null +++ b/docs/plans/2026-02-02-variant-trait-design.md @@ -0,0 +1,176 @@ +# Variant Trait Design + +## Overview + +Replace `type GraphType` and `type Weight` associated types in `Problem` trait with a single `fn variant()` method that returns extensible key-value attributes. + +## Motivation + +- Current design has fixed `GraphType` and `Weight` associated types +- Not extensible for other variant attributes (e.g., `k` for k-SAT, density) +- `variant()` method provides uniform, extensible interface + +## Design + +### Core Trait Changes + +```rust +// src/traits.rs + +pub trait Problem: Clone { + /// Base name of this problem type (e.g., "IndependentSet"). + const NAME: &'static str; + + /// The type used for objective/size values. + type Size: Clone + PartialOrd + Num + Zero + AddAssign; + + /// Returns attributes describing this problem variant. + /// Each (key, value) pair describes a variant dimension. + /// Common keys: "graph", "weight" + fn variant() -> Vec<(&'static str, &'static str)>; + + fn num_variables(&self) -> usize; + fn num_flavors(&self) -> usize; + fn problem_size(&self) -> ProblemSize; + fn energy_mode(&self) -> EnergyMode; + fn solution_size(&self, config: &[usize]) -> SolutionSize; + // ... default methods unchanged +} +``` + +### Helper Function + +```rust +// src/variant.rs + +use std::any::type_name; + +/// Extract short type name from full path. +/// e.g., "problemreductions::graph_types::SimpleGraph" -> "SimpleGraph" +pub fn short_type_name() -> &'static str { + let full = type_name::(); + full.rsplit("::").next().unwrap_or(full) +} +``` + +### Problem Implementation Pattern + +```rust +// src/models/graph/independent_set.rs + +use crate::variant::short_type_name; + +pub struct IndependentSet { + graph: UnGraph<(), ()>, + weights: Vec, + _phantom: PhantomData, +} + +impl Problem for IndependentSet +where + W: Clone + Default + PartialOrd + Num + Zero + AddAssign + 'static, + G: GraphMarker, +{ + const NAME: &'static str = "IndependentSet"; + type Size = W; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", short_type_name::()), + ("weight", short_type_name::()), + ] + } + + // ... other methods unchanged +} +``` + +### Registry Updates + +```rust +// src/rules/registry.rs + +pub struct ReductionEntry { + /// Base name of source problem (e.g., "IndependentSet"). + pub source_name: &'static str, + /// Base name of target problem (e.g., "VertexCovering"). + pub target_name: &'static str, + /// Source variant attributes. + pub source_variant: &'static [(&'static str, &'static str)], + /// Target variant attributes. + pub target_variant: &'static [(&'static str, &'static str)], + /// Function to create overhead information. + pub overhead_fn: fn() -> ReductionOverhead, +} +``` + +### Reduction Graph JSON Format + +```json +{ + "nodes": [ + { + "name": "IndependentSet", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + }, + { + "name": "VertexCovering", + "variant": { + "graph": "SimpleGraph", + "weight": "Unweighted" + } + } + ], + "edges": [ + { + "source": { + "name": "IndependentSet", + "variant": { "graph": "SimpleGraph", "weight": "Unweighted" } + }, + "target": { + "name": "VertexCovering", + "variant": { "graph": "SimpleGraph", "weight": "Unweighted" } + } + } + ] +} +``` + +## Changes Summary + +### Remove +- `type GraphType: GraphMarker` from `Problem` trait +- `type Weight: NumericWeight` from `Problem` trait +- `GraphMarker::NAME` constant +- `variant_id()` function +- `source_graph`, `target_graph`, `source_weighted`, `target_weighted` from `ReductionEntry` + +### Add +- `fn variant() -> Vec<(&'static str, &'static str)>` to `Problem` trait +- `short_type_name()` helper function in `src/variant.rs` +- `source_variant`, `target_variant` fields in `ReductionEntry` + +### Keep +- `GraphMarker` trait (for subtype relationships and type bounds) +- `NumericWeight` trait (for type bounds) +- Type parameters on structs (e.g., `IndependentSet`) + +### Update +- All `Problem` implementations to add `variant()` method +- Reduction graph JSON to use structured variant dict +- Graph building code in `src/rules/graph.rs` +- `#[reduction]` macro in `problemreductions-macros` + +## Files Affected + +1. `src/traits.rs` - Remove associated types, add `variant()` method +2. `src/variant.rs` - New file with `short_type_name()` helper +3. `src/graph_types.rs` - Remove `NAME` from `GraphMarker` +4. `src/rules/registry.rs` - Update `ReductionEntry` structure +5. `src/rules/graph.rs` - Update graph building to use structured variants +6. `src/models/**/*.rs` - Update all Problem implementations (~20 files) +7. `problemreductions-macros/src/lib.rs` - Update reduction macro +8. `docs/paper/reduction_graph.json` - Regenerate with new format diff --git a/docs/plans/2026-02-02-variant-trait-implementation.md b/docs/plans/2026-02-02-variant-trait-implementation.md new file mode 100644 index 0000000..40e1029 --- /dev/null +++ b/docs/plans/2026-02-02-variant-trait-implementation.md @@ -0,0 +1,1059 @@ +# Variant Trait Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace `type GraphType` and `type Weight` associated types with extensible `fn variant()` method. + +**Architecture:** Modify `Problem` trait to use `fn variant() -> Vec<(&'static str, &'static str)>` instead of associated types. Use `std::any::type_name` to extract type names at runtime. Update registry and graph export to use structured variant dict. + +**Tech Stack:** Rust, std::any::type_name, serde_json + +--- + +### Task 1: Add variant helper module + +**Files:** +- Create: `src/variant.rs` +- Modify: `src/lib.rs` + +**Step 1: Create src/variant.rs with short_type_name helper** + +```rust +//! Variant attribute utilities. + +use std::any::type_name; + +/// Extract short type name from full path. +/// e.g., "problemreductions::graph_types::SimpleGraph" -> "SimpleGraph" +pub fn short_type_name() -> &'static str { + let full = type_name::(); + full.rsplit("::").next().unwrap_or(full) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_short_type_name_primitive() { + assert_eq!(short_type_name::(), "i32"); + assert_eq!(short_type_name::(), "f64"); + } + + #[test] + fn test_short_type_name_struct() { + struct MyStruct; + assert_eq!(short_type_name::(), "MyStruct"); + } +} +``` + +**Step 2: Add module to src/lib.rs** + +Add after other module declarations: +```rust +pub mod variant; +``` + +**Step 3: Run tests** + +Run: `cargo test variant --lib` +Expected: PASS + +**Step 4: Commit** + +```bash +git add src/variant.rs src/lib.rs +git commit -m "feat: add variant helper module with short_type_name" +``` + +--- + +### Task 2: Update Problem trait + +**Files:** +- Modify: `src/traits.rs` + +**Step 1: Remove GraphType and Weight, add variant() method** + +In `src/traits.rs`, update the `Problem` trait: + +Remove these lines: +```rust + /// The graph type this problem operates on. + type GraphType: GraphMarker; + + /// The weight type for this problem. + type Weight: NumericWeight; +``` + +Add this method after `const NAME`: +```rust + /// Returns attributes describing this problem variant. + /// Each (key, value) pair describes a variant dimension. + /// Common keys: "graph", "weight" + fn variant() -> Vec<(&'static str, &'static str)>; +``` + +Remove the import of `GraphMarker` from the use statement: +```rust +use crate::graph_types::GraphMarker; +``` + +Remove `NumericWeight` from the use statement if it's only used for the Weight bound. + +**Step 2: Update test problems in traits.rs** + +Update `SimpleWeightedProblem` impl: +```rust +impl Problem for SimpleWeightedProblem { + const NAME: &'static str = "SimpleWeightedProblem"; + type Size = i32; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } + // ... rest unchanged +} +``` + +Update `SimpleCsp` impl: +```rust +impl Problem for SimpleCsp { + const NAME: &'static str = "SimpleCsp"; + type Size = i32; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } + // ... rest unchanged +} +``` + +Update `MultiFlavorProblem` impl: +```rust +impl Problem for MultiFlavorProblem { + const NAME: &'static str = "MultiFlavorProblem"; + type Size = i32; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } + // ... rest unchanged +} +``` + +**Step 3: Verify traits.rs compiles** + +Run: `cargo check --lib` +Expected: Errors in model files (expected, will fix in next tasks) + +**Step 4: Commit** + +```bash +git add src/traits.rs +git commit -m "feat: replace GraphType/Weight with variant() in Problem trait" +``` + +--- + +### Task 3: Update graph problem models + +**Files:** +- Modify: `src/models/graph/independent_set.rs` +- Modify: `src/models/graph/vertex_covering.rs` +- Modify: `src/models/graph/dominating_set.rs` +- Modify: `src/models/graph/matching.rs` +- Modify: `src/models/graph/max_cut.rs` +- Modify: `src/models/graph/coloring.rs` +- Modify: `src/models/graph/maximal_is.rs` + +**Step 1: Update independent_set.rs** + +Add import at top: +```rust +use crate::variant::short_type_name; +``` + +In `impl Problem for IndependentSet`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = W; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } +``` + +**Step 2: Update vertex_covering.rs** + +Add import at top: +```rust +use crate::variant::short_type_name; +``` + +In `impl Problem for VertexCovering`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = W; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } +``` + +**Step 3: Update dominating_set.rs** + +Add import at top: +```rust +use crate::variant::short_type_name; +``` + +In `impl Problem for DominatingSet`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = W; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } +``` + +**Step 4: Update matching.rs** + +Add import at top: +```rust +use crate::variant::short_type_name; +``` + +In `impl Problem for Matching`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = W; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } +``` + +**Step 5: Update max_cut.rs** + +Add import at top: +```rust +use crate::variant::short_type_name; +``` + +In `impl Problem for MaxCut`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = W; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } +``` + +**Step 6: Update coloring.rs** + +In `impl Problem for Coloring`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = i32; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } +``` + +**Step 7: Update maximal_is.rs** + +In `impl Problem for MaximalIS`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = i32; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } +``` + +**Step 8: Verify compilation** + +Run: `cargo check --lib` +Expected: More errors (other models still need updating) + +**Step 9: Commit** + +```bash +git add src/models/graph/ +git commit -m "feat: update graph models to use variant()" +``` + +--- + +### Task 4: Update satisfiability models + +**Files:** +- Modify: `src/models/satisfiability/sat.rs` +- Modify: `src/models/satisfiability/ksat.rs` + +**Step 1: Update sat.rs** + +Add import at top: +```rust +use crate::variant::short_type_name; +``` + +In `impl Problem for Satisfiability`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = W; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } +``` + +**Step 2: Update ksat.rs** + +Add import at top: +```rust +use crate::variant::short_type_name; +``` + +In `impl Problem for KSatisfiability`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = W; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } +``` + +**Step 3: Commit** + +```bash +git add src/models/satisfiability/ +git commit -m "feat: update satisfiability models to use variant()" +``` + +--- + +### Task 5: Update set models + +**Files:** +- Modify: `src/models/set/set_packing.rs` +- Modify: `src/models/set/set_covering.rs` + +**Step 1: Update set_packing.rs** + +Add import at top: +```rust +use crate::variant::short_type_name; +``` + +In `impl Problem for SetPacking`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = W; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } +``` + +**Step 2: Update set_covering.rs** + +Add import at top: +```rust +use crate::variant::short_type_name; +``` + +In `impl Problem for SetCovering`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = W; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } +``` + +**Step 3: Commit** + +```bash +git add src/models/set/ +git commit -m "feat: update set models to use variant()" +``` + +--- + +### Task 6: Update optimization models + +**Files:** +- Modify: `src/models/optimization/spin_glass.rs` +- Modify: `src/models/optimization/qubo.rs` +- Modify: `src/models/optimization/ilp.rs` + +**Step 1: Update spin_glass.rs** + +Add import at top: +```rust +use crate::variant::short_type_name; +``` + +In `impl Problem for SpinGlass`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = W; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } +``` + +**Step 2: Update qubo.rs** + +Add import at top: +```rust +use crate::variant::short_type_name; +``` + +In `impl Problem for QUBO`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = W; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } +``` + +**Step 3: Update ilp.rs** + +In `impl Problem for ILP`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = f64; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "f64"), + ] + } +``` + +**Step 4: Commit** + +```bash +git add src/models/optimization/ +git commit -m "feat: update optimization models to use variant()" +``` + +--- + +### Task 7: Update specialized models + +**Files:** +- Modify: `src/models/specialized/circuit.rs` +- Modify: `src/models/specialized/factoring.rs` +- Modify: `src/models/specialized/biclique_cover.rs` +- Modify: `src/models/specialized/bmf.rs` +- Modify: `src/models/specialized/paintshop.rs` + +**Step 1: Update circuit.rs** + +Add import at top: +```rust +use crate::variant::short_type_name; +``` + +In `impl Problem for CircuitSAT`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = W; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } +``` + +**Step 2: Update factoring.rs** + +In `impl Problem for Factoring`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = i32; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } +``` + +**Step 3: Update biclique_cover.rs** + +In `impl Problem for BicliqueCover`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = i32; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } +``` + +**Step 4: Update bmf.rs** + +In `impl Problem for BMF`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = i32; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } +``` + +**Step 5: Update paintshop.rs** + +In `impl Problem for PaintShop`, replace: +```rust + type GraphType = SimpleGraph; + type Weight = i32; +``` + +With: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } +``` + +**Step 6: Commit** + +```bash +git add src/models/specialized/ +git commit -m "feat: update specialized models to use variant()" +``` + +--- + +### Task 8: Update template.rs GraphProblem + +**Files:** +- Modify: `src/models/graph/template.rs` + +**Step 1: Update GraphProblem impl** + +In `impl Problem for GraphProblem`, replace: +```rust + const NAME: &'static str = C::NAME; + type GraphType = SimpleGraphMarker; + type Weight = W; +``` + +With: +```rust + const NAME: &'static str = C::NAME; + type Size = W; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", crate::variant::short_type_name::()), + ] + } +``` + +**Step 2: Commit** + +```bash +git add src/models/graph/template.rs +git commit -m "feat: update GraphProblem template to use variant()" +``` + +--- + +### Task 9: Update solver test problems + +**Files:** +- Modify: `src/solvers/brute_force.rs` + +**Step 1: Update test problem implementations** + +Find all `impl Problem for` blocks in the test module and replace `type GraphType` and `type Weight` with `fn variant()`. + +For `MaxSumProblem`: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "i32")] + } +``` + +For `MinSumProblem`: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "i32")] + } +``` + +For `SelectAtMostOneProblem`: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "i32")] + } +``` + +For `FloatProblem`: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "f64")] + } +``` + +For `NearlyEqualProblem`: +```rust + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "f64")] + } +``` + +**Step 2: Commit** + +```bash +git add src/solvers/brute_force.rs +git commit -m "feat: update solver test problems to use variant()" +``` + +--- + +### Task 10: Update ReductionEntry in registry + +**Files:** +- Modify: `src/rules/registry.rs` + +**Step 1: Update ReductionEntry struct** + +Replace: +```rust +pub struct ReductionEntry { + pub source_name: &'static str, + pub target_name: &'static str, + pub source_graph: &'static str, + pub target_graph: &'static str, + pub source_weighted: bool, + pub target_weighted: bool, + pub overhead_fn: fn() -> ReductionOverhead, +} +``` + +With: +```rust +pub struct ReductionEntry { + pub source_name: &'static str, + pub target_name: &'static str, + pub source_variant: &'static [(&'static str, &'static str)], + pub target_variant: &'static [(&'static str, &'static str)], + pub overhead_fn: fn() -> ReductionOverhead, +} +``` + +**Step 2: Remove variant_id function and related methods** + +Delete the `variant_id` function and update/remove `source_variant_id()` and `target_variant_id()` methods. + +**Step 3: Update Debug impl** + +```rust +impl std::fmt::Debug for ReductionEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ReductionEntry") + .field("source_name", &self.source_name) + .field("target_name", &self.target_name) + .field("source_variant", &self.source_variant) + .field("target_variant", &self.target_variant) + .field("overhead", &self.overhead()) + .finish() + } +} +``` + +**Step 4: Update tests** + +Update test `test_reduction_entry_overhead`: +```rust +let entry = ReductionEntry { + source_name: "TestSource", + target_name: "TestTarget", + source_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")], + target_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")], + overhead_fn: || ReductionOverhead::new(vec![("n", poly!(2 * n))]), +}; +``` + +Remove tests: `test_variant_id_base`, `test_variant_id_graph`, `test_variant_id_weighted`, `test_variant_id_both`, `test_entry_variant_ids`. + +Update `test_reduction_entries_registered` to not use variant_id. + +**Step 5: Commit** + +```bash +git add src/rules/registry.rs +git commit -m "feat: update ReductionEntry to use variant slices" +``` + +--- + +### Task 11: Update graph.rs JSON export + +**Files:** +- Modify: `src/rules/graph.rs` + +**Step 1: Update NodeJson struct** + +Replace: +```rust +pub struct NodeJson { + pub id: String, + pub label: String, + pub category: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub parent: Option, + pub graph_type: String, + pub weighted: bool, +} +``` + +With: +```rust +use std::collections::BTreeMap; + +pub struct NodeJson { + pub name: String, + pub variant: BTreeMap, + pub category: String, +} +``` + +**Step 2: Update EdgeJson struct** + +Replace: +```rust +pub struct EdgeJson { + pub source: String, + pub target: String, + pub bidirectional: bool, +} +``` + +With: +```rust +#[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash)] +pub struct VariantRef { + pub name: String, + pub variant: BTreeMap, +} + +pub struct EdgeJson { + pub source: VariantRef, + pub target: VariantRef, + pub bidirectional: bool, +} +``` + +**Step 3: Update to_json() method** + +Update the `to_json()` method in `ReductionGraph` to build nodes and edges using the new structured format. Use `BTreeMap` to convert variant slices to maps. + +**Step 4: Commit** + +```bash +git add src/rules/graph.rs +git commit -m "feat: update graph JSON export to structured variant format" +``` + +--- + +### Task 12: Update reduction macro + +**Files:** +- Modify: `problemreductions-macros/src/lib.rs` + +**Step 1: Update ReductionAttrs struct** + +Replace graph/weighted attributes with variant: +```rust +struct ReductionAttrs { + source_variant: Option>, + target_variant: Option>, + overhead: Option, +} +``` + +**Step 2: Update parsing** + +Update the Parse impl to handle `source_variant` and `target_variant` as arrays. + +**Step 3: Update code generation** + +Update `generate_reduction_entry` to output: +```rust +inventory::submit! { + crate::rules::registry::ReductionEntry { + source_name: #source_name, + target_name: #target_name, + source_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")], + target_variant: &[("graph", "SimpleGraph"), ("weight", "i32")], + overhead_fn: || { #overhead }, + } +} +``` + +**Step 4: Commit** + +```bash +git add problemreductions-macros/src/lib.rs +git commit -m "feat: update reduction macro for variant slices" +``` + +--- + +### Task 13: Update all reduction rule files + +**Files:** +- Modify: All files in `src/rules/` that use `inventory::submit!` + +**Step 1: Find all reduction registrations** + +Run: `grep -l "inventory::submit" src/rules/*.rs` + +**Step 2: Update each file** + +For each file, update the `inventory::submit!` block to use the new format: +```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(...), + } +} +``` + +**Step 3: Commit** + +```bash +git add src/rules/ +git commit -m "feat: update all reduction registrations to variant format" +``` + +--- + +### Task 14: Remove GraphMarker::NAME + +**Files:** +- Modify: `src/graph_types.rs` + +**Step 1: Remove NAME from GraphMarker trait** + +Remove: +```rust + const NAME: &'static str; +``` + +**Step 2: Remove NAME from all impls** + +Remove the `const NAME` line from: +- `impl GraphMarker for SimpleGraph` +- `impl GraphMarker for PlanarGraph` +- `impl GraphMarker for UnitDiskGraph` +- `impl GraphMarker for BipartiteGraph` + +**Step 3: Update any code that uses GraphMarker::NAME** + +Search for usages and replace with `short_type_name::()` calls. + +**Step 4: Commit** + +```bash +git add src/graph_types.rs +git commit -m "feat: remove NAME from GraphMarker trait" +``` + +--- + +### Task 15: Run full test suite and fix issues + +**Step 1: Run cargo check** + +Run: `cargo check --all-features` +Fix any compilation errors. + +**Step 2: Run tests** + +Run: `cargo test --all-features` +Fix any test failures. + +**Step 3: Run clippy** + +Run: `cargo clippy --all-features` +Fix any warnings. + +**Step 4: Commit fixes** + +```bash +git add -A +git commit -m "fix: resolve compilation and test issues" +``` + +--- + +### Task 16: Regenerate reduction graph + +**Step 1: Run export-graph** + +Run: `make export-graph` + +**Step 2: Verify JSON format** + +Check `docs/paper/reduction_graph.json` has the new structured format. + +**Step 3: Commit** + +```bash +git add docs/paper/reduction_graph.json +git commit -m "chore: regenerate reduction graph with new variant format" +``` + +--- + +### Task 17: Final verification + +**Step 1: Run full CI checks** + +Run: `make test clippy` + +**Step 2: Verify all tests pass** + +Expected: All tests pass, no clippy warnings. diff --git a/problemreductions-macros/Cargo.toml b/problemreductions-macros/Cargo.toml new file mode 100644 index 0000000..50e8cf0 --- /dev/null +++ b/problemreductions-macros/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "problemreductions-macros" +version = "0.1.0" +edition = "2021" +description = "Procedural macros for problemreductions" +license = "MIT" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full", "parsing"] } +quote = "1.0" +proc-macro2 = "1.0" diff --git a/problemreductions-macros/src/lib.rs b/problemreductions-macros/src/lib.rs new file mode 100644 index 0000000..13d3f1e --- /dev/null +++ b/problemreductions-macros/src/lib.rs @@ -0,0 +1,314 @@ +//! Procedural macros for problemreductions. +//! +//! This crate provides the `#[reduction]` attribute macro that automatically +//! generates `ReductionEntry` registrations from `ReduceTo` impl blocks. + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{parse_macro_input, GenericArgument, ItemImpl, Path, PathArguments, Type}; + +/// Attribute macro for automatic reduction registration. +/// +/// This macro parses a `ReduceTo` impl block and automatically generates +/// the corresponding `inventory::submit!` call with the correct metadata. +/// +/// # Type Parameter Convention +/// +/// The macro extracts weight and graph type information from type parameters: +/// - `Problem` where `W` is a type parameter - weighted if W != Unweighted +/// - `Problem` where `G` is a graph type - extracts graph type name +/// +/// # Example +/// +/// ```ignore +/// #[reduction( +/// source_graph = "SimpleGraph", +/// target_graph = "GridGraph", +/// source_weighted = false, +/// target_weighted = true, +/// )] +/// impl ReduceTo> for IndependentSet { +/// type Result = ReductionISToGridIS; +/// fn reduce_to(&self) -> Self::Result { ... } +/// } +/// ``` +/// +/// The macro also supports inferring from type names when explicit attributes aren't provided. +#[proc_macro_attribute] +pub fn reduction(attr: TokenStream, item: TokenStream) -> TokenStream { + let attrs = parse_macro_input!(attr as ReductionAttrs); + let impl_block = parse_macro_input!(item as ItemImpl); + + match generate_reduction_entry(&attrs, &impl_block) { + Ok(tokens) => tokens.into(), + Err(e) => e.to_compile_error().into(), + } +} + +/// Parsed attributes from #[reduction(...)] +struct ReductionAttrs { + source_graph: Option, + target_graph: Option, + source_weighted: Option, + target_weighted: Option, + overhead: Option, +} + +impl syn::parse::Parse for ReductionAttrs { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut attrs = ReductionAttrs { + source_graph: None, + target_graph: None, + source_weighted: None, + target_weighted: None, + overhead: None, + }; + + while !input.is_empty() { + let ident: syn::Ident = input.parse()?; + input.parse::()?; + + match ident.to_string().as_str() { + "source_graph" => { + let lit: syn::LitStr = input.parse()?; + attrs.source_graph = Some(lit.value()); + } + "target_graph" => { + let lit: syn::LitStr = input.parse()?; + attrs.target_graph = Some(lit.value()); + } + "source_weighted" => { + let lit: syn::LitBool = input.parse()?; + attrs.source_weighted = Some(lit.value()); + } + "target_weighted" => { + let lit: syn::LitBool = input.parse()?; + attrs.target_weighted = Some(lit.value()); + } + "overhead" => { + let content; + syn::braced!(content in input); + attrs.overhead = Some(content.parse()?); + } + _ => { + return Err(syn::Error::new( + ident.span(), + format!("unknown attribute: {}", ident), + )); + } + } + + if input.peek(syn::Token![,]) { + input.parse::()?; + } + } + + Ok(attrs) + } +} + +/// Extract the base type name from a Type (e.g., "IndependentSet" from "IndependentSet") +fn extract_type_name(ty: &Type) -> Option { + match ty { + Type::Path(type_path) => { + let segment = type_path.path.segments.last()?; + Some(segment.ident.to_string()) + } + _ => None, + } +} + +/// Extract graph type from type parameters (second parameter if present) +fn extract_graph_type(ty: &Type) -> Option { + match ty { + Type::Path(type_path) => { + let segment = type_path.path.segments.last()?; + if let PathArguments::AngleBracketed(args) = &segment.arguments { + // Count only type arguments (skip const generics) + let mut type_arg_index = 0; + for arg in args.args.iter() { + if let GenericArgument::Type(inner_ty) = arg { + if let Type::Path(inner_path) = inner_ty { + let name = inner_path + .path + .segments + .last() + .map(|s| s.ident.to_string())?; + // Common graph type names - explicit matches + if name.ends_with("Graph") || name == "CNF" || name == "SetSystem" { + return Some(name); + } + // If it's the second TYPE parameter and looks like a concrete graph type + // (not a single-letter generic like W, T, G or a known weight type) + if type_arg_index == 1 + && !is_weight_or_generic_param(&name) + { + return Some(name); + } + } + type_arg_index += 1; + } + // Const generics (GenericArgument::Const) are skipped automatically + } + } + None + } + _ => None, + } +} + +/// Check if a type name looks like a weight type or a generic type parameter +fn is_weight_or_generic_param(name: &str) -> bool { + // Known weight types + if ["i32", "i64", "f32", "f64", "Unweighted"].contains(&name) { + return true; + } + // Single uppercase letter - typically a generic type parameter (W, T, G, etc.) + if name.len() == 1 && name.chars().next().map(|c| c.is_ascii_uppercase()).unwrap_or(false) { + return true; + } + false +} + +/// Extract weight type from first type parameter +fn extract_weight_type(ty: &Type) -> Option { + match ty { + Type::Path(type_path) => { + let segment = type_path.path.segments.last()?; + if let PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(GenericArgument::Type(inner_ty)) = args.args.first() { + return Some(inner_ty.clone()); + } + } + None + } + _ => None, + } +} + +/// Get weight type name as a string for the variant. +/// Single-letter uppercase names are treated as generic type parameters +/// and default to "Unweighted" since they're not concrete types. +fn get_weight_name(ty: &Type) -> String { + match ty { + Type::Path(type_path) => { + let name = type_path + .path + .segments + .last() + .map(|s| s.ident.to_string()) + .unwrap_or_else(|| "Unweighted".to_string()); + // Treat single uppercase letters as generic params, default to Unweighted + if name.len() == 1 && name.chars().next().map(|c| c.is_ascii_uppercase()).unwrap_or(false) { + "Unweighted".to_string() + } else { + name + } + } + _ => "Unweighted".to_string(), + } +} + +/// Generate the reduction entry code +fn generate_reduction_entry( + attrs: &ReductionAttrs, + impl_block: &ItemImpl, +) -> syn::Result { + // Extract the trait path (should be ReduceTo) + let trait_path = impl_block + .trait_ + .as_ref() + .map(|(_, path, _)| path) + .ok_or_else(|| syn::Error::new_spanned(impl_block, "Expected impl ReduceTo for S"))?; + + // Extract target type from ReduceTo + let target_type = extract_target_from_trait(trait_path)?; + + // Extract source type (Self type) + let source_type = &impl_block.self_ty; + + // Get type names + let source_name = extract_type_name(source_type) + .ok_or_else(|| syn::Error::new_spanned(source_type, "Cannot extract source type name"))?; + let target_name = extract_type_name(&target_type) + .ok_or_else(|| syn::Error::new_spanned(&target_type, "Cannot extract target type name"))?; + + // Determine weight type names + let source_weight_name = attrs.source_weighted.map(|w| { + if w { "i32".to_string() } else { "Unweighted".to_string() } + }).unwrap_or_else(|| { + extract_weight_type(source_type) + .map(|t| get_weight_name(&t)) + .unwrap_or_else(|| "Unweighted".to_string()) + }); + let target_weight_name = attrs.target_weighted.map(|w| { + if w { "i32".to_string() } else { "Unweighted".to_string() } + }).unwrap_or_else(|| { + extract_weight_type(&target_type) + .map(|t| get_weight_name(&t)) + .unwrap_or_else(|| "Unweighted".to_string()) + }); + + // Determine graph types + let source_graph = attrs + .source_graph + .clone() + .or_else(|| extract_graph_type(source_type)) + .unwrap_or_else(|| "SimpleGraph".to_string()); + let target_graph = attrs + .target_graph + .clone() + .or_else(|| extract_graph_type(&target_type)) + .unwrap_or_else(|| "SimpleGraph".to_string()); + + // Generate overhead or use default + let overhead = attrs.overhead.clone().unwrap_or_else(|| { + quote! { + crate::rules::registry::ReductionOverhead::default() + } + }); + + // Generate the combined output + let output = quote! { + #impl_block + + inventory::submit! { + crate::rules::registry::ReductionEntry { + source_name: #source_name, + target_name: #target_name, + source_variant: &[("graph", #source_graph), ("weight", #source_weight_name)], + target_variant: &[("graph", #target_graph), ("weight", #target_weight_name)], + overhead_fn: || { #overhead }, + } + } + }; + + Ok(output) +} + +/// Extract the target type from ReduceTo trait path +fn extract_target_from_trait(path: &Path) -> syn::Result { + let segment = path + .segments + .last() + .ok_or_else(|| syn::Error::new_spanned(path, "Empty trait path"))?; + + if segment.ident != "ReduceTo" { + return Err(syn::Error::new_spanned( + segment, + "Expected ReduceTo trait", + )); + } + + if let PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(GenericArgument::Type(ty)) = args.args.first() { + return Ok(ty.clone()); + } + } + + Err(syn::Error::new_spanned( + segment, + "Expected ReduceTo with type parameter", + )) +} diff --git a/src/graph_types.rs b/src/graph_types.rs index 490ce31..2ec5ea0 100644 --- a/src/graph_types.rs +++ b/src/graph_types.rs @@ -3,10 +3,7 @@ use inventory; /// Marker trait for graph types. -pub trait GraphMarker: 'static + Clone + Send + Sync { - /// The name of this graph type for runtime queries. - const NAME: &'static str; -} +pub trait GraphMarker: 'static + Clone + Send + Sync {} /// Compile-time subtype relationship between graph types. pub trait GraphSubtype: GraphMarker {} @@ -18,33 +15,25 @@ impl GraphSubtype for G {} #[derive(Debug, Clone, Copy, Default)] pub struct SimpleGraph; -impl GraphMarker for SimpleGraph { - const NAME: &'static str = "SimpleGraph"; -} +impl GraphMarker for SimpleGraph {} /// Planar graph - can be drawn on a plane without edge crossings. #[derive(Debug, Clone, Copy, Default)] pub struct PlanarGraph; -impl GraphMarker for PlanarGraph { - const NAME: &'static str = "PlanarGraph"; -} +impl GraphMarker for PlanarGraph {} /// Unit disk graph - vertices are points, edges connect points within unit distance. #[derive(Debug, Clone, Copy, Default)] pub struct UnitDiskGraph; -impl GraphMarker for UnitDiskGraph { - const NAME: &'static str = "UnitDiskGraph"; -} +impl GraphMarker for UnitDiskGraph {} /// Bipartite graph - vertices can be partitioned into two sets with edges only between sets. #[derive(Debug, Clone, Copy, Default)] pub struct BipartiteGraph; -impl GraphMarker for BipartiteGraph { - const NAME: &'static str = "BipartiteGraph"; -} +impl GraphMarker for BipartiteGraph {} /// Runtime registration of graph subtype relationships. pub struct GraphSubtypeEntry { @@ -62,8 +51,8 @@ macro_rules! declare_graph_subtype { ::inventory::submit! { $crate::graph_types::GraphSubtypeEntry { - subtype: <$sub as $crate::graph_types::GraphMarker>::NAME, - supertype: <$sup as $crate::graph_types::GraphMarker>::NAME, + subtype: stringify!($sub), + supertype: stringify!($sup), } } }; @@ -81,14 +70,6 @@ declare_graph_subtype!(BipartiteGraph => SimpleGraph); mod tests { use super::*; - #[test] - fn test_graph_marker_names() { - assert_eq!(SimpleGraph::NAME, "SimpleGraph"); - assert_eq!(PlanarGraph::NAME, "PlanarGraph"); - assert_eq!(UnitDiskGraph::NAME, "UnitDiskGraph"); - assert_eq!(BipartiteGraph::NAME, "BipartiteGraph"); - } - #[test] fn test_reflexive_subtype() { fn assert_subtype, B: GraphMarker>() {} diff --git a/src/lib.rs b/src/lib.rs index 16cd6d8..e3cdbdb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,6 +74,7 @@ pub mod topology; pub mod traits; pub mod truth_table; pub mod types; +pub mod variant; /// Prelude module for convenient imports. pub mod prelude { @@ -107,3 +108,6 @@ pub use registry::{ComplexityClass, ProblemCategory, ProblemInfo}; pub use solvers::{BruteForce, Solver}; pub use traits::{ConstraintSatisfactionProblem, Problem}; pub use types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; + +// Re-export proc macro for reduction registration +pub use problemreductions_macros::reduction; diff --git a/src/models/graph/coloring.rs b/src/models/graph/coloring.rs index 483513e..28bf012 100644 --- a/src/models/graph/coloring.rs +++ b/src/models/graph/coloring.rs @@ -3,7 +3,6 @@ //! The K-Coloring problem asks whether a graph can be colored with K colors //! such that no two adjacent vertices have the same color. -use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use petgraph::graph::{NodeIndex, UnGraph}; @@ -98,8 +97,14 @@ impl Coloring { impl Problem for Coloring { const NAME: &'static str = "Coloring"; - type GraphType = SimpleGraph; - type Weight = i32; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } + type Size = i32; fn num_variables(&self) -> usize { diff --git a/src/models/graph/dominating_set.rs b/src/models/graph/dominating_set.rs index b9ede6b..f85fcb8 100644 --- a/src/models/graph/dominating_set.rs +++ b/src/models/graph/dominating_set.rs @@ -3,8 +3,8 @@ //! The Dominating Set problem asks for a minimum weight subset of vertices //! such that every vertex is either in the set or adjacent to a vertex in the set. -use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; +use crate::variant::short_type_name; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use petgraph::graph::{NodeIndex, UnGraph}; use serde::{Deserialize, Serialize}; @@ -128,8 +128,14 @@ where + 'static, { const NAME: &'static str = "DominatingSet"; - type GraphType = SimpleGraph; - type Weight = W; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } + type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/graph/independent_set.rs b/src/models/graph/independent_set.rs index da117ae..10b6699 100644 --- a/src/models/graph/independent_set.rs +++ b/src/models/graph/independent_set.rs @@ -3,8 +3,8 @@ //! The Independent Set problem asks for a maximum weight subset of vertices //! such that no two vertices in the subset are adjacent. -use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; +use crate::variant::short_type_name; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use petgraph::graph::{NodeIndex, UnGraph}; use petgraph::visit::EdgeRef; @@ -121,8 +121,14 @@ where + 'static, { const NAME: &'static str = "IndependentSet"; - type GraphType = SimpleGraph; - type Weight = W; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } + type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/graph/matching.rs b/src/models/graph/matching.rs index 842dd02..9794d8b 100644 --- a/src/models/graph/matching.rs +++ b/src/models/graph/matching.rs @@ -3,8 +3,8 @@ //! The Maximum Matching problem asks for a maximum weight set of edges //! such that no two edges share a vertex. -use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; +use crate::variant::short_type_name; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use petgraph::graph::{NodeIndex, UnGraph}; use petgraph::visit::EdgeRef; @@ -143,8 +143,14 @@ where + 'static, { const NAME: &'static str = "Matching"; - type GraphType = SimpleGraph; - type Weight = W; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } + type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/graph/max_cut.rs b/src/models/graph/max_cut.rs index f85e3fb..45a2176 100644 --- a/src/models/graph/max_cut.rs +++ b/src/models/graph/max_cut.rs @@ -3,8 +3,8 @@ //! The Maximum Cut problem asks for a partition of vertices into two sets //! that maximizes the total weight of edges crossing the partition. -use crate::graph_types::SimpleGraph; use crate::traits::Problem; +use crate::variant::short_type_name; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use petgraph::graph::{NodeIndex, UnGraph}; use petgraph::visit::EdgeRef; @@ -139,8 +139,14 @@ where + 'static, { const NAME: &'static str = "MaxCut"; - type GraphType = SimpleGraph; - type Weight = W; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } + type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/graph/maximal_is.rs b/src/models/graph/maximal_is.rs index 8117438..d37c951 100644 --- a/src/models/graph/maximal_is.rs +++ b/src/models/graph/maximal_is.rs @@ -3,7 +3,6 @@ //! The Maximal Independent Set problem asks for an independent set that //! cannot be extended by adding any other vertex. -use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use petgraph::graph::{NodeIndex, UnGraph}; @@ -114,8 +113,14 @@ impl MaximalIS { impl Problem for MaximalIS { const NAME: &'static str = "MaximalIS"; - type GraphType = SimpleGraph; - type Weight = i32; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } + type Size = i32; fn num_variables(&self) -> usize { diff --git a/src/models/graph/template.rs b/src/models/graph/template.rs index ffc3640..770b1c7 100644 --- a/src/models/graph/template.rs +++ b/src/models/graph/template.rs @@ -71,13 +71,13 @@ //! - **Vertex Cover**: `[false, true, true, true]` - at least one selected //! - **Perfect Matching**: Define on edge graph with exactly one selected -use crate::graph_types::SimpleGraph as SimpleGraphMarker; use crate::registry::{ ComplexityClass, GraphSubcategory, ProblemCategory, ProblemInfo, ProblemMetadata, }; use crate::topology::{Graph, SimpleGraph}; use crate::traits::{ConstraintSatisfactionProblem, Problem}; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; +use crate::variant::short_type_name; use num_traits::{Num, Zero}; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; @@ -319,8 +319,14 @@ where W: Clone + Default + PartialOrd + Num + Zero + AddAssign + 'static, { const NAME: &'static str = C::NAME; - type GraphType = SimpleGraphMarker; - type Weight = W; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } + type Size = W; fn num_variables(&self) -> usize { @@ -719,4 +725,35 @@ mod tests { let cat = CliqueConstraint::category(); assert_eq!(cat.path(), "graph/independent"); } + + #[test] + fn test_variant_for_graph_problem() { + use crate::traits::Problem; + + // Test IndependentSetT variant + let v = IndependentSetT::::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0], ("graph", "SimpleGraph")); + assert_eq!(v[1], ("weight", "i32")); + + // Test with f64 weight + let v = IndependentSetT::::variant(); + assert_eq!(v[1], ("weight", "f64")); + + // Test VertexCoverT variant + let v = VertexCoverT::::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0], ("graph", "SimpleGraph")); + + // Test CliqueT variant + let v = CliqueT::::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0], ("graph", "SimpleGraph")); + + // Test with UnitDiskGraph + let v = IndependentSetT::::variant(); + assert_eq!(v.len(), 2); + // Note: variant() returns "SimpleGraph" as hardcoded, not the actual graph type + assert_eq!(v[0], ("graph", "SimpleGraph")); + } } diff --git a/src/models/graph/vertex_covering.rs b/src/models/graph/vertex_covering.rs index ecd216b..a9c98c7 100644 --- a/src/models/graph/vertex_covering.rs +++ b/src/models/graph/vertex_covering.rs @@ -3,8 +3,8 @@ //! The Vertex Cover problem asks for a minimum weight subset of vertices //! such that every edge has at least one endpoint in the subset. -use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; +use crate::variant::short_type_name; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use petgraph::graph::{NodeIndex, UnGraph}; use petgraph::visit::EdgeRef; @@ -106,8 +106,14 @@ where + 'static, { const NAME: &'static str = "VertexCovering"; - type GraphType = SimpleGraph; - type Weight = W; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } + type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/optimization/ilp.rs b/src/models/optimization/ilp.rs index 7c662d1..98a57fb 100644 --- a/src/models/optimization/ilp.rs +++ b/src/models/optimization/ilp.rs @@ -3,7 +3,6 @@ //! ILP optimizes a linear objective over integer variables subject to linear constraints. //! This is a fundamental "hub" problem that many other NP-hard problems can be reduced to. -use crate::graph_types::SimpleGraph; use crate::traits::Problem; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -327,8 +326,14 @@ impl ILP { impl Problem for ILP { const NAME: &'static str = "ILP"; - type GraphType = SimpleGraph; - type Weight = f64; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "f64"), + ] + } + type Size = f64; fn num_variables(&self) -> usize { @@ -956,4 +961,12 @@ mod tests { // Config [1,1,1] => [1, 0, 6] assert_eq!(ilp.config_to_values(&[1, 1, 1]), vec![1, 0, 6]); } + + #[test] + fn test_ilp_variant() { + let v = ILP::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0], ("graph", "SimpleGraph")); + assert_eq!(v[1], ("weight", "f64")); + } } diff --git a/src/models/optimization/qubo.rs b/src/models/optimization/qubo.rs index d2dac8e..620130e 100644 --- a/src/models/optimization/qubo.rs +++ b/src/models/optimization/qubo.rs @@ -2,8 +2,8 @@ //! //! QUBO minimizes a quadratic function over binary variables. -use crate::graph_types::SimpleGraph; use crate::traits::Problem; +use crate::variant::short_type_name; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -113,8 +113,14 @@ where + 'static, { const NAME: &'static str = "QUBO"; - type GraphType = SimpleGraph; - type Weight = W; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } + type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/optimization/spin_glass.rs b/src/models/optimization/spin_glass.rs index 246056a..cef5e56 100644 --- a/src/models/optimization/spin_glass.rs +++ b/src/models/optimization/spin_glass.rs @@ -2,8 +2,8 @@ //! //! The Spin Glass problem minimizes the Ising Hamiltonian energy. -use crate::graph_types::SimpleGraph; use crate::traits::Problem; +use crate::variant::short_type_name; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -110,8 +110,14 @@ where + 'static, { const NAME: &'static str = "SpinGlass"; - type GraphType = SimpleGraph; - type Weight = W; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } + type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/satisfiability/ksat.rs b/src/models/satisfiability/ksat.rs index 3dc2353..f50d774 100644 --- a/src/models/satisfiability/ksat.rs +++ b/src/models/satisfiability/ksat.rs @@ -3,8 +3,8 @@ //! K-SAT is a special case of SAT where each clause has exactly K literals. //! Common variants include 3-SAT (K=3) and 2-SAT (K=2). -use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; +use crate::variant::short_type_name; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -178,8 +178,14 @@ where + 'static, { const NAME: &'static str = "KSatisfiability"; - type GraphType = SimpleGraph; - type Weight = W; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } + type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/satisfiability/sat.rs b/src/models/satisfiability/sat.rs index 430dbcf..0c17c4b 100644 --- a/src/models/satisfiability/sat.rs +++ b/src/models/satisfiability/sat.rs @@ -3,8 +3,8 @@ //! SAT is the problem of determining if there exists an assignment of //! Boolean variables that makes a given Boolean formula true. -use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; +use crate::variant::short_type_name; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -184,8 +184,14 @@ where + 'static, { const NAME: &'static str = "Satisfiability"; - type GraphType = SimpleGraph; - type Weight = W; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } + type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/set/set_covering.rs b/src/models/set/set_covering.rs index 284d70e..03ee07d 100644 --- a/src/models/set/set_covering.rs +++ b/src/models/set/set_covering.rs @@ -3,8 +3,8 @@ //! The Set Covering problem asks for a minimum weight collection of sets //! that covers all elements in the universe. -use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; +use crate::variant::short_type_name; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -121,8 +121,14 @@ where + 'static, { const NAME: &'static str = "SetCovering"; - type GraphType = SimpleGraph; - type Weight = W; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } + type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/set/set_packing.rs b/src/models/set/set_packing.rs index 7e5a356..c96450d 100644 --- a/src/models/set/set_packing.rs +++ b/src/models/set/set_packing.rs @@ -3,8 +3,8 @@ //! The Set Packing problem asks for a maximum weight collection of //! pairwise disjoint sets. -use crate::graph_types::SimpleGraph; use crate::traits::{ConstraintSatisfactionProblem, Problem}; +use crate::variant::short_type_name; use crate::types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -117,8 +117,14 @@ where + 'static, { const NAME: &'static str = "SetPacking"; - type GraphType = SimpleGraph; - type Weight = W; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } + type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/specialized/biclique_cover.rs b/src/models/specialized/biclique_cover.rs index bec17f7..56ac9a3 100644 --- a/src/models/specialized/biclique_cover.rs +++ b/src/models/specialized/biclique_cover.rs @@ -3,7 +3,6 @@ //! The Biclique Cover problem asks for the minimum number of bicliques //! (complete bipartite subgraphs) needed to cover all edges of a bipartite graph. -use crate::graph_types::SimpleGraph; use crate::traits::Problem; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -183,8 +182,14 @@ impl BicliqueCover { impl Problem for BicliqueCover { const NAME: &'static str = "BicliqueCover"; - type GraphType = SimpleGraph; - type Weight = i32; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } + type Size = i32; fn num_variables(&self) -> usize { diff --git a/src/models/specialized/bmf.rs b/src/models/specialized/bmf.rs index 233aa5a..4d5fb12 100644 --- a/src/models/specialized/bmf.rs +++ b/src/models/specialized/bmf.rs @@ -4,7 +4,6 @@ //! the boolean product B ⊙ C approximates A. //! The boolean product `(B ⊙ C)[i,j] = OR_k (B[i,k] AND C[k,j])`. -use crate::graph_types::SimpleGraph; use crate::traits::Problem; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -159,8 +158,14 @@ impl BMF { impl Problem for BMF { const NAME: &'static str = "BMF"; - type GraphType = SimpleGraph; - type Weight = i32; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } + type Size = i32; fn num_variables(&self) -> usize { diff --git a/src/models/specialized/circuit.rs b/src/models/specialized/circuit.rs index 9140362..7512c20 100644 --- a/src/models/specialized/circuit.rs +++ b/src/models/specialized/circuit.rs @@ -3,8 +3,8 @@ //! CircuitSAT represents a boolean circuit satisfiability problem. //! The goal is to find variable assignments that satisfy the circuit constraints. -use crate::graph_types::SimpleGraph; use crate::traits::Problem; +use crate::variant::short_type_name; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -278,8 +278,14 @@ where + 'static, { const NAME: &'static str = "CircuitSAT"; - type GraphType = SimpleGraph; - type Weight = W; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", short_type_name::()), + ] + } + type Size = W; fn num_variables(&self) -> usize { diff --git a/src/models/specialized/factoring.rs b/src/models/specialized/factoring.rs index 6366941..fb946bd 100644 --- a/src/models/specialized/factoring.rs +++ b/src/models/specialized/factoring.rs @@ -3,7 +3,6 @@ //! The Factoring problem represents integer factorization as a computational problem. //! Given a number N, find two factors (a, b) such that a * b = N. -use crate::graph_types::SimpleGraph; use crate::traits::Problem; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -97,8 +96,14 @@ fn int_to_bits(n: u64, num_bits: usize) -> Vec { impl Problem for Factoring { const NAME: &'static str = "Factoring"; - type GraphType = SimpleGraph; - type Weight = i32; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } + type Size = i32; fn num_variables(&self) -> usize { diff --git a/src/models/specialized/paintshop.rs b/src/models/specialized/paintshop.rs index 2ada14d..570b328 100644 --- a/src/models/specialized/paintshop.rs +++ b/src/models/specialized/paintshop.rs @@ -5,7 +5,6 @@ //! one color at its first occurrence and another at its second. //! The goal is to minimize color switches between adjacent positions. -use crate::graph_types::SimpleGraph; use crate::traits::Problem; use crate::types::{EnergyMode, ProblemSize, SolutionSize}; use serde::{Deserialize, Serialize}; @@ -143,8 +142,14 @@ impl PaintShop { impl Problem for PaintShop { const NAME: &'static str = "PaintShop"; - type GraphType = SimpleGraph; - type Weight = i32; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } + type Size = i32; fn num_variables(&self) -> usize { diff --git a/src/rules/circuit_spinglass.rs b/src/rules/circuit_spinglass.rs index 38af890..be5bca2 100644 --- a/src/rules/circuit_spinglass.rs +++ b/src/rules/circuit_spinglass.rs @@ -8,6 +8,9 @@ use crate::models::optimization::SpinGlass; use crate::models::specialized::{Assignment, BooleanExpr, BooleanOp, CircuitSAT}; +use crate::poly; +use crate::reduction; +use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::traits::Problem; use crate::types::ProblemSize; @@ -418,6 +421,15 @@ where } } +#[reduction( + target_graph = "SimpleGraph", + overhead = { + ReductionOverhead::new(vec![ + ("num_spins", poly!(num_assignments)), + ("num_interactions", poly!(num_assignments)), + ]) + } +)] impl ReduceTo> for CircuitSAT where W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, @@ -971,19 +983,3 @@ mod tests { } } -// Register reduction with inventory for auto-discovery -use crate::poly; -use crate::rules::registry::{ReductionEntry, ReductionOverhead}; - -inventory::submit! { - ReductionEntry { - source_name: "CircuitSAT", - target_name: "SpinGlass", - source_graph: "Circuit", - target_graph: "SpinGlassGraph", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_spins", poly!(num_assignments)), - ("num_interactions", poly!(num_assignments)), - ]), - } -} diff --git a/src/rules/coloring_ilp.rs b/src/rules/coloring_ilp.rs index 75bc46b..cd5b568 100644 --- a/src/rules/coloring_ilp.rs +++ b/src/rules/coloring_ilp.rs @@ -20,8 +20,8 @@ inventory::submit! { ReductionEntry { source_name: "Coloring", target_name: "ILP", - source_graph: "SimpleGraph", - target_graph: "ILPMatrix", + source_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")], + target_variant: &[("graph", ""), ("weight", "Unweighted")], overhead_fn: || ReductionOverhead::new(vec![ // num_vars = num_vertices * num_colors ("num_vars", Polynomial { diff --git a/src/rules/factoring_circuit.rs b/src/rules/factoring_circuit.rs index be20d98..375b66e 100644 --- a/src/rules/factoring_circuit.rs +++ b/src/rules/factoring_circuit.rs @@ -8,6 +8,9 @@ //! carry propagation, building up partial products row by row. use crate::models::specialized::{Assignment, BooleanExpr, Circuit, CircuitSAT, Factoring}; +use crate::poly; +use crate::reduction; +use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::traits::Problem; use crate::types::ProblemSize; @@ -186,6 +189,11 @@ fn build_multiplier_cell( (assignments, ancillas) } +#[reduction(overhead = { + ReductionOverhead::new(vec![ + ("num_gates", poly!(num_bits_first^2)), + ]) +})] impl ReduceTo> for Factoring { type Result = ReductionFactoringToCircuit; @@ -576,18 +584,3 @@ mod tests { } } -// Register reduction with inventory for auto-discovery -use crate::poly; -use crate::rules::registry::{ReductionEntry, ReductionOverhead}; - -inventory::submit! { - ReductionEntry { - source_name: "Factoring", - target_name: "CircuitSAT", - source_graph: "Factoring", - target_graph: "Circuit", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_gates", poly!(num_bits_first^2)), - ]), - } -} diff --git a/src/rules/factoring_ilp.rs b/src/rules/factoring_ilp.rs index 055cb95..72d6a7b 100644 --- a/src/rules/factoring_ilp.rs +++ b/src/rules/factoring_ilp.rs @@ -31,8 +31,8 @@ inventory::submit! { ReductionEntry { source_name: "Factoring", target_name: "ILP", - source_graph: "Factoring", - target_graph: "ILPMatrix", + source_variant: &[("graph", ""), ("weight", "Unweighted")], + target_variant: &[("graph", ""), ("weight", "Unweighted")], overhead_fn: || ReductionOverhead::new(vec![ // num_vars = m + n + m*n + num_carries where num_carries = max(m+n, target_bits) // For feasible instances, target_bits <= m+n, so this is 2(m+n) + m*n diff --git a/src/rules/graph.rs b/src/rules/graph.rs index 98b2f7a..b07282a 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -33,21 +33,30 @@ pub struct ReductionGraphJson { /// A node in the reduction graph JSON. #[derive(Debug, Clone, Serialize)] pub struct NodeJson { - /// Unique identifier for the node (base type name). - pub id: String, - /// Display label for the node. - pub label: String, + /// Base problem name (e.g., "IndependentSet"). + pub name: String, + /// Variant attributes as key-value pairs. + pub variant: std::collections::BTreeMap, /// Category of the problem (e.g., "graph", "set", "optimization", "satisfiability", "specialized"). pub category: String, } +/// Reference to a problem variant in an edge. +#[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash)] +pub struct VariantRef { + /// Base problem name. + pub name: String, + /// Variant attributes as key-value pairs. + pub variant: std::collections::BTreeMap, +} + /// An edge in the reduction graph JSON. #[derive(Debug, Clone, Serialize)] pub struct EdgeJson { - /// Source node ID. - pub source: String, - /// Target node ID. - pub target: String, + /// Source problem variant. + pub source: VariantRef, + /// Target problem variant. + pub target: VariantRef, /// Whether the reverse reduction also exists. pub bidirectional: bool, } @@ -88,14 +97,38 @@ impl ReductionPath { /// Edge data for a reduction. #[derive(Clone, Debug)] pub struct ReductionEdge { - /// Graph type of source problem (e.g., "SimpleGraph"). - pub source_graph: &'static str, - /// Graph type of target problem. - pub target_graph: &'static str, + /// Source variant attributes as key-value pairs. + pub source_variant: &'static [(&'static str, &'static str)], + /// Target variant attributes as key-value pairs. + pub target_variant: &'static [(&'static str, &'static str)], /// Overhead information for cost calculations. pub overhead: ReductionOverhead, } +impl ReductionEdge { + /// Get the graph type from the source variant, or "SimpleGraph" as default. + /// Empty strings are treated as missing and default to "SimpleGraph". + pub fn source_graph(&self) -> &'static str { + self.source_variant + .iter() + .find(|(k, _)| *k == "graph") + .map(|(_, v)| *v) + .filter(|v| !v.is_empty()) + .unwrap_or("SimpleGraph") + } + + /// Get the graph type from the target variant, or "SimpleGraph" as default. + /// Empty strings are treated as missing and default to "SimpleGraph". + pub fn target_graph(&self) -> &'static str { + self.target_variant + .iter() + .find(|(k, _)| *k == "graph") + .map(|(_, v)| *v) + .filter(|v| !v.is_empty()) + .unwrap_or("SimpleGraph") + } +} + /// Runtime graph of all registered reductions. /// /// Uses type-erased names for the graph topology, so `MaxCut` and `MaxCut` @@ -154,8 +187,8 @@ impl ReductionGraph { src, dst, ReductionEdge { - source_graph: entry.source_graph, - target_graph: entry.target_graph, + source_variant: entry.source_variant, + target_variant: entry.target_variant, overhead: entry.overhead(), }, ); @@ -351,7 +384,7 @@ impl ReductionGraph { let next = edge_ref.target(); // Check set-theoretic applicability - if !self.rule_applicable(source.1, target.1, edge.source_graph, edge.target_graph) { + if !self.rule_applicable(source.1, target.1, edge.source_graph(), edge.target_graph()) { continue; } @@ -500,53 +533,104 @@ impl Default for ReductionGraph { } impl ReductionGraph { + /// Helper to convert a variant slice to a BTreeMap. + /// Normalizes empty "graph" values to "SimpleGraph" for consistency. + fn variant_to_map( + variant: &[(&'static str, &'static str)], + ) -> std::collections::BTreeMap { + variant + .iter() + .map(|(k, v)| { + let value = if *k == "graph" && v.is_empty() { + "SimpleGraph".to_string() + } else { + v.to_string() + }; + (k.to_string(), value) + }) + .collect() + } + + /// Helper to create a VariantRef from name and variant slice. + fn make_variant_ref( + name: &str, + variant: &[(&'static str, &'static str)], + ) -> VariantRef { + VariantRef { + name: name.to_string(), + variant: Self::variant_to_map(variant), + } + } + /// Export the reduction graph as a JSON-serializable structure. + /// + /// This method generates nodes for each variant based on the registered reductions. pub fn to_json(&self) -> ReductionGraphJson { - // Collect all edges first to determine bidirectionality - let mut edge_set: HashMap<(&str, &str), bool> = HashMap::new(); - - for edge in self.graph.edge_indices() { - if let Some((src_idx, dst_idx)) = self.graph.edge_endpoints(edge) { - let src_name = self.graph[src_idx]; - let dst_name = self.graph[dst_idx]; - - // Check if reverse edge exists - let reverse_key = (dst_name, src_name); - if edge_set.contains_key(&reverse_key) { - // Mark the existing edge as bidirectional - edge_set.insert(reverse_key, true); - } else { - edge_set.insert((src_name, dst_name), false); - } - } + use crate::rules::registry::ReductionEntry; + + // Collect all unique nodes (name + variant combination) + let mut node_set: HashSet<(String, std::collections::BTreeMap)> = + HashSet::new(); + + // First, add base nodes from the graph + for &name in self.name_indices.keys() { + node_set.insert((name.to_string(), std::collections::BTreeMap::new())); + } + + // Then, collect variants from reduction entries + for entry in inventory::iter:: { + node_set.insert(( + entry.source_name.to_string(), + Self::variant_to_map(entry.source_variant), + )); + node_set.insert(( + entry.target_name.to_string(), + Self::variant_to_map(entry.target_variant), + )); } - // Build nodes with categories, sorted by id for deterministic output - let mut nodes: Vec = self - .name_indices - .keys() - .map(|&name| { + // Build nodes with categories + let mut nodes: Vec = node_set + .iter() + .map(|(name, variant)| { let category = Self::categorize_type(name); NodeJson { - id: name.to_string(), - label: name.to_string(), // Base name is already simplified + name: name.clone(), + variant: variant.clone(), category: category.to_string(), } }) .collect(); - nodes.sort_by(|a, b| a.id.cmp(&b.id)); + nodes.sort_by(|a, b| (&a.name, &a.variant).cmp(&(&b.name, &b.variant))); - // Build edges (only include one direction for bidirectional edges) - // Sort by (source, target) for deterministic output + // Collect edges, checking for bidirectionality + let mut edge_set: HashMap<(VariantRef, VariantRef), bool> = HashMap::new(); + + for entry in inventory::iter:: { + let src_ref = Self::make_variant_ref(entry.source_name, entry.source_variant); + let dst_ref = Self::make_variant_ref(entry.target_name, entry.target_variant); + + let reverse_key = (dst_ref.clone(), src_ref.clone()); + if edge_set.contains_key(&reverse_key) { + edge_set.insert(reverse_key, true); + } else { + edge_set.insert((src_ref, dst_ref), false); + } + } + + // Build edges let mut edges: Vec = edge_set .into_iter() .map(|((src, dst), bidirectional)| EdgeJson { - source: src.to_string(), - target: dst.to_string(), + source: src, + target: dst, bidirectional, }) .collect(); - edges.sort_by(|a, b| (&a.source, &a.target).cmp(&(&b.source, &b.target))); + edges.sort_by(|a, b| { + (&a.source.name, &a.source.variant, &a.target.name, &a.target.variant) + .cmp(&(&b.source.name, &b.source.variant, &b.target.name, &b.target.variant)) + }); ReductionGraphJson { nodes, edges } } @@ -719,7 +803,7 @@ mod tests { // Check nodes assert!(json.nodes.len() >= 10); - assert!(json.nodes.iter().any(|n| n.label == "IndependentSet")); + assert!(json.nodes.iter().any(|n| n.name == "IndependentSet")); assert!(json.nodes.iter().any(|n| n.category == "graph")); assert!(json.nodes.iter().any(|n| n.category == "optimization")); @@ -728,8 +812,8 @@ mod tests { // Check that IS <-> VC is marked bidirectional let is_vc_edge = json.edges.iter().find(|e| { - (e.source.contains("IndependentSet") && e.target.contains("VertexCovering")) - || (e.source.contains("VertexCovering") && e.target.contains("IndependentSet")) + (e.source.name.contains("IndependentSet") && e.target.name.contains("VertexCovering")) + || (e.source.name.contains("VertexCovering") && e.target.name.contains("IndependentSet")) }); assert!(is_vc_edge.is_some()); assert!(is_vc_edge.unwrap().bidirectional); @@ -1006,15 +1090,15 @@ mod tests { // Verify specific known bidirectional edges let is_vc_bidir = json.edges.iter().any(|e| { - (e.source.contains("IndependentSet") && e.target.contains("VertexCovering") - || e.source.contains("VertexCovering") && e.target.contains("IndependentSet")) + (e.source.name.contains("IndependentSet") && e.target.name.contains("VertexCovering") + || e.source.name.contains("VertexCovering") && e.target.name.contains("IndependentSet")) && e.bidirectional }); assert!(is_vc_bidir, "IS <-> VC should be bidirectional"); // Verify specific known unidirectional edge let factoring_circuit_unidir = json.edges.iter().any(|e| { - e.source.contains("Factoring") && e.target.contains("CircuitSAT") && !e.bidirectional + e.source.name.contains("Factoring") && e.target.name.contains("CircuitSAT") && !e.bidirectional }); assert!( factoring_circuit_unidir, @@ -1155,11 +1239,11 @@ mod tests { let input_size = ProblemSize::new(vec![("num_vertices", 10), ("num_edges", 20)]); // Find multi-step path where all edges use compatible graph types - // IndependentSet (SimpleGraph) -> SetPacking (SetSystem) -> IndependentSet (SimpleGraph) - // This tests the algorithm can find multi-step paths with consistent graph types + // IndependentSet (SimpleGraph) -> SetPacking (SimpleGraph) + // This tests the algorithm can find paths with consistent graph types let path = graph.find_cheapest_path( ("IndependentSet", "SimpleGraph"), - ("SetPacking", "SetSystem"), + ("SetPacking", "SimpleGraph"), &input_size, &cost_fn, ); @@ -1221,12 +1305,92 @@ mod tests { #[test] fn test_reduction_edge_struct() { let edge = ReductionEdge { - source_graph: "PlanarGraph", - target_graph: "SimpleGraph", + source_variant: &[("graph", "PlanarGraph"), ("weight", "Unweighted")], + target_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")], + overhead: ReductionOverhead::default(), + }; + + assert_eq!(edge.source_graph(), "PlanarGraph"); + assert_eq!(edge.target_graph(), "SimpleGraph"); + } + + #[test] + fn test_reduction_edge_default_graph() { + // When no "graph" key is present, default to SimpleGraph + let edge = ReductionEdge { + source_variant: &[("weight", "Unweighted")], + target_variant: &[], overhead: ReductionOverhead::default(), }; - assert_eq!(edge.source_graph, "PlanarGraph"); - assert_eq!(edge.target_graph, "SimpleGraph"); + assert_eq!(edge.source_graph(), "SimpleGraph"); + assert_eq!(edge.target_graph(), "SimpleGraph"); + } + + #[test] + fn test_variant_to_map() { + let variant: &[(&str, &str)] = &[("graph", "SimpleGraph"), ("weight", "i32")]; + let map = ReductionGraph::variant_to_map(variant); + assert_eq!(map.get("graph"), Some(&"SimpleGraph".to_string())); + assert_eq!(map.get("weight"), Some(&"i32".to_string())); + assert_eq!(map.len(), 2); + } + + #[test] + fn test_variant_to_map_empty() { + let variant: &[(&str, &str)] = &[]; + let map = ReductionGraph::variant_to_map(variant); + assert!(map.is_empty()); + } + + #[test] + fn test_make_variant_ref() { + let variant: &[(&str, &str)] = &[("graph", "PlanarGraph"), ("weight", "f64")]; + let variant_ref = ReductionGraph::make_variant_ref("IndependentSet", variant); + assert_eq!(variant_ref.name, "IndependentSet"); + assert_eq!(variant_ref.variant.get("graph"), Some(&"PlanarGraph".to_string())); + assert_eq!(variant_ref.variant.get("weight"), Some(&"f64".to_string())); + } + + #[test] + fn test_to_json_nodes_have_variants() { + let graph = ReductionGraph::new(); + let json = graph.to_json(); + + // Check that nodes have variant information + for node in &json.nodes { + // Verify node has a name + assert!(!node.name.is_empty()); + // Verify node has a category + assert!(!node.category.is_empty()); + } + } + + #[test] + fn test_to_json_edges_have_variants() { + let graph = ReductionGraph::new(); + let json = graph.to_json(); + + // Check that edges have source and target variant refs + for edge in &json.edges { + assert!(!edge.source.name.is_empty()); + assert!(!edge.target.name.is_empty()); + } + } + + #[test] + fn test_json_variant_content() { + let graph = ReductionGraph::new(); + let json = graph.to_json(); + + // Find a node and verify its variant contains expected keys + let is_node = json.nodes.iter().find(|n| n.name == "IndependentSet"); + assert!(is_node.is_some(), "IndependentSet node should exist"); + + // Find an edge involving IndependentSet (could be source or target) + let is_edge = json.edges.iter().find(|e| { + e.source.name == "IndependentSet" || e.target.name == "IndependentSet" + }); + assert!(is_edge.is_some(), "Edge involving IndependentSet should exist"); } } diff --git a/src/rules/independentset_setpacking.rs b/src/rules/independentset_setpacking.rs index 184c754..9efbfb9 100644 --- a/src/rules/independentset_setpacking.rs +++ b/src/rules/independentset_setpacking.rs @@ -5,6 +5,9 @@ use crate::models::graph::IndependentSet; use crate::models::set::SetPacking; +use crate::poly; +use crate::reduction; +use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::traits::Problem; use crate::types::ProblemSize; @@ -44,6 +47,15 @@ where } } +#[reduction( + source_graph = "SimpleGraph", + overhead = { + ReductionOverhead::new(vec![ + ("num_sets", poly!(num_vertices)), + ("num_elements", poly!(num_vertices)), + ]) + } +)] impl ReduceTo> for IndependentSet where W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, @@ -102,6 +114,15 @@ where } } +#[reduction( + target_graph = "SimpleGraph", + overhead = { + ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_sets)), + ("num_edges", poly!(num_sets)), + ]) + } +)] impl ReduceTo> for SetPacking where W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, @@ -271,32 +292,3 @@ mod tests { } } -// Register reductions with inventory for auto-discovery -use crate::poly; -use crate::rules::registry::{ReductionEntry, ReductionOverhead}; - -inventory::submit! { - ReductionEntry { - source_name: "IndependentSet", - target_name: "SetPacking", - source_graph: "SimpleGraph", - target_graph: "SetSystem", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_sets", poly!(num_vertices)), - ("num_elements", poly!(num_vertices)), - ]), - } -} - -inventory::submit! { - ReductionEntry { - source_name: "SetPacking", - target_name: "IndependentSet", - source_graph: "SetSystem", - target_graph: "SimpleGraph", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_sets)), - ("num_edges", poly!(num_sets)), - ]), - } -} diff --git a/src/rules/matching_setpacking.rs b/src/rules/matching_setpacking.rs index 3e21a5f..9ca7de2 100644 --- a/src/rules/matching_setpacking.rs +++ b/src/rules/matching_setpacking.rs @@ -5,6 +5,9 @@ use crate::models::graph::Matching; use crate::models::set::SetPacking; +use crate::poly; +use crate::reduction; +use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::traits::{ConstraintSatisfactionProblem, Problem}; use crate::types::ProblemSize; @@ -43,6 +46,15 @@ where } } +#[reduction( + source_graph = "SimpleGraph", + overhead = { + ReductionOverhead::new(vec![ + ("num_sets", poly!(num_edges)), + ("num_elements", poly!(num_vertices)), + ]) + } +)] impl ReduceTo> for Matching where W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, @@ -260,19 +272,3 @@ mod tests { } } -// Register reduction with inventory for auto-discovery -use crate::poly; -use crate::rules::registry::{ReductionEntry, ReductionOverhead}; - -inventory::submit! { - ReductionEntry { - source_name: "Matching", - target_name: "SetPacking", - source_graph: "SimpleGraph", - target_graph: "SetSystem", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_sets", poly!(num_edges)), - ("num_elements", poly!(num_vertices)), - ]), - } -} diff --git a/src/rules/registry.rs b/src/rules/registry.rs index 7b44f39..5983aa2 100644 --- a/src/rules/registry.rs +++ b/src/rules/registry.rs @@ -39,10 +39,11 @@ pub struct ReductionEntry { pub source_name: &'static str, /// Base name of target problem (e.g., "VertexCovering"). pub target_name: &'static str, - /// Graph type of source problem (e.g., "SimpleGraph"). - pub source_graph: &'static str, - /// Graph type of target problem. - pub target_graph: &'static str, + /// Variant attributes for source problem as key-value pairs. + /// Common keys: "graph" (graph type), "weight" (weight type). + pub source_variant: &'static [(&'static str, &'static str)], + /// Variant attributes for target problem as key-value pairs. + pub target_variant: &'static [(&'static str, &'static str)], /// Function to create overhead information (lazy evaluation for static context). pub overhead_fn: fn() -> ReductionOverhead, } @@ -52,6 +53,23 @@ impl ReductionEntry { pub fn overhead(&self) -> ReductionOverhead { (self.overhead_fn)() } + + /// Check if this reduction involves only the base (unweighted) variants. + pub fn is_base_reduction(&self) -> bool { + let source_unweighted = self + .source_variant + .iter() + .find(|(k, _)| *k == "weight") + .map(|(_, v)| *v == "Unweighted") + .unwrap_or(true); + let target_unweighted = self + .target_variant + .iter() + .find(|(k, _)| *k == "weight") + .map(|(_, v)| *v == "Unweighted") + .unwrap_or(true); + source_unweighted && target_unweighted + } } impl std::fmt::Debug for ReductionEntry { @@ -59,8 +77,8 @@ impl std::fmt::Debug for ReductionEntry { f.debug_struct("ReductionEntry") .field("source_name", &self.source_name) .field("target_name", &self.target_name) - .field("source_graph", &self.source_graph) - .field("target_graph", &self.target_graph) + .field("source_variant", &self.source_variant) + .field("target_variant", &self.target_variant) .field("overhead", &self.overhead()) .finish() } @@ -95,8 +113,8 @@ mod tests { let entry = ReductionEntry { source_name: "TestSource", target_name: "TestTarget", - source_graph: "SimpleGraph", - target_graph: "SimpleGraph", + source_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")], + target_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")], overhead_fn: || ReductionOverhead::new(vec![("n", poly!(2 * n))]), }; @@ -111,8 +129,8 @@ mod tests { let entry = ReductionEntry { source_name: "A", target_name: "B", - source_graph: "SimpleGraph", - target_graph: "SimpleGraph", + source_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")], + target_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")], overhead_fn: || ReductionOverhead::default(), }; @@ -121,6 +139,67 @@ mod tests { assert!(debug_str.contains("B")); } + #[test] + fn test_is_base_reduction_unweighted() { + let entry = ReductionEntry { + source_name: "A", + target_name: "B", + source_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")], + target_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")], + overhead_fn: || ReductionOverhead::default(), + }; + assert!(entry.is_base_reduction()); + } + + #[test] + fn test_is_base_reduction_source_weighted() { + let entry = ReductionEntry { + source_name: "A", + target_name: "B", + source_variant: &[("graph", "SimpleGraph"), ("weight", "i32")], + target_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")], + overhead_fn: || ReductionOverhead::default(), + }; + assert!(!entry.is_base_reduction()); + } + + #[test] + fn test_is_base_reduction_target_weighted() { + let entry = ReductionEntry { + source_name: "A", + target_name: "B", + source_variant: &[("graph", "SimpleGraph"), ("weight", "Unweighted")], + target_variant: &[("graph", "SimpleGraph"), ("weight", "f64")], + overhead_fn: || ReductionOverhead::default(), + }; + assert!(!entry.is_base_reduction()); + } + + #[test] + fn test_is_base_reduction_both_weighted() { + let entry = ReductionEntry { + source_name: "A", + target_name: "B", + source_variant: &[("graph", "SimpleGraph"), ("weight", "i32")], + target_variant: &[("graph", "SimpleGraph"), ("weight", "f64")], + overhead_fn: || ReductionOverhead::default(), + }; + assert!(!entry.is_base_reduction()); + } + + #[test] + fn test_is_base_reduction_no_weight_key() { + // If no weight key is present, assume unweighted (base) + let entry = ReductionEntry { + source_name: "A", + target_name: "B", + source_variant: &[("graph", "SimpleGraph")], + target_variant: &[("graph", "SimpleGraph")], + overhead_fn: || ReductionOverhead::default(), + }; + assert!(entry.is_base_reduction()); + } + #[test] fn test_reduction_entries_registered() { let entries: Vec<_> = inventory::iter::().collect(); diff --git a/src/rules/sat_coloring.rs b/src/rules/sat_coloring.rs index b7b6c4f..825e8b0 100644 --- a/src/rules/sat_coloring.rs +++ b/src/rules/sat_coloring.rs @@ -10,6 +10,9 @@ use crate::models::graph::Coloring; use crate::models::satisfiability::Satisfiability; +use crate::poly; +use crate::reduction; +use crate::rules::registry::ReductionOverhead; use crate::rules::sat_independentset::BoolVar; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::traits::Problem; @@ -311,6 +314,15 @@ impl ReductionSATToColoring { } } +#[reduction( + target_graph = "SimpleGraph", + overhead = { + ReductionOverhead::new(vec![ + ("num_vertices", poly!(3 * num_vars)), + ("num_colors", poly!(3)), + ]) + } +)] impl ReduceTo for Satisfiability where W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, @@ -647,19 +659,3 @@ mod tests { } } -// Register reduction with inventory for auto-discovery -use crate::poly; -use crate::rules::registry::{ReductionEntry, ReductionOverhead}; - -inventory::submit! { - ReductionEntry { - source_name: "Satisfiability", - target_name: "Coloring", - source_graph: "CNF", - target_graph: "SimpleGraph", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_vertices", poly!(3 * num_vars)), - ("num_colors", poly!(3)), - ]), - } -} diff --git a/src/rules/sat_dominatingset.rs b/src/rules/sat_dominatingset.rs index 3871fd9..43589ef 100644 --- a/src/rules/sat_dominatingset.rs +++ b/src/rules/sat_dominatingset.rs @@ -16,6 +16,9 @@ use crate::models::graph::DominatingSet; use crate::models::satisfiability::Satisfiability; +use crate::poly; +use crate::reduction; +use crate::rules::registry::ReductionOverhead; use crate::rules::sat_independentset::BoolVar; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::traits::Problem; @@ -126,6 +129,15 @@ impl ReductionSATToDS { } } +#[reduction( + target_graph = "SimpleGraph", + overhead = { + ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_vars)), + ("num_edges", poly!(num_clauses)), + ]) + } +)] impl ReduceTo> for Satisfiability where W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, @@ -509,19 +521,3 @@ mod tests { } } -// Register reduction with inventory for auto-discovery -use crate::poly; -use crate::rules::registry::{ReductionEntry, ReductionOverhead}; - -inventory::submit! { - ReductionEntry { - source_name: "Satisfiability", - target_name: "DominatingSet", - source_graph: "CNF", - target_graph: "SimpleGraph", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_vars)), - ("num_edges", poly!(num_clauses)), - ]), - } -} diff --git a/src/rules/sat_independentset.rs b/src/rules/sat_independentset.rs index 6a9f549..8a51ad5 100644 --- a/src/rules/sat_independentset.rs +++ b/src/rules/sat_independentset.rs @@ -10,6 +10,9 @@ use crate::models::graph::IndependentSet; use crate::models::satisfiability::Satisfiability; +use crate::poly; +use crate::reduction; +use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::traits::Problem; use crate::types::ProblemSize; @@ -122,6 +125,15 @@ impl ReductionSATToIS { } } +#[reduction( + target_graph = "SimpleGraph", + overhead = { + ReductionOverhead::new(vec![ + ("num_vertices", poly!(7 * num_clauses)), + ("num_edges", poly!(21 * num_clauses)), + ]) + } +)] impl ReduceTo> for Satisfiability where W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, @@ -491,19 +503,3 @@ mod tests { } } -// Register reduction with inventory for auto-discovery -use crate::poly; -use crate::rules::registry::{ReductionEntry, ReductionOverhead}; - -inventory::submit! { - ReductionEntry { - source_name: "Satisfiability", - target_name: "IndependentSet", - source_graph: "CNF", - target_graph: "SimpleGraph", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_vertices", poly!(7 * num_clauses)), - ("num_edges", poly!(21 * num_clauses)), - ]), - } -} diff --git a/src/rules/sat_ksat.rs b/src/rules/sat_ksat.rs index b577ea1..95b13e7 100644 --- a/src/rules/sat_ksat.rs +++ b/src/rules/sat_ksat.rs @@ -7,6 +7,9 @@ //! K-SAT -> SAT: Trivial embedding (K-SAT is a special case of SAT) use crate::models::satisfiability::{CNFClause, KSatisfiability, Satisfiability}; +use crate::poly; +use crate::reduction; +use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::traits::Problem; use crate::types::ProblemSize; @@ -197,6 +200,12 @@ where } } +#[reduction(overhead = { + ReductionOverhead::new(vec![ + ("num_clauses", poly!(num_clauses)), + ("num_vars", poly!(num_vars)), + ]) +})] impl ReduceTo> for KSatisfiability where W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, @@ -548,29 +557,13 @@ mod tests { } } -// Register reductions with inventory for auto-discovery -use crate::poly; -use crate::rules::registry::{ReductionEntry, ReductionOverhead}; - +// Register SAT -> KSAT reduction manually (generated by macro, can't use #[reduction]) inventory::submit! { - ReductionEntry { + crate::rules::registry::ReductionEntry { source_name: "Satisfiability", target_name: "KSatisfiability", - source_graph: "CNF", - target_graph: "KCNF", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_clauses", poly!(num_clauses)), - ("num_vars", poly!(num_vars)), - ]), - } -} - -inventory::submit! { - ReductionEntry { - source_name: "KSatisfiability", - target_name: "Satisfiability", - source_graph: "KCNF", - target_graph: "CNF", + source_variant: &[("graph", ""), ("weight", "Unweighted")], + target_variant: &[("graph", ""), ("weight", "Unweighted")], overhead_fn: || ReductionOverhead::new(vec![ ("num_clauses", poly!(num_clauses)), ("num_vars", poly!(num_vars)), diff --git a/src/rules/spinglass_maxcut.rs b/src/rules/spinglass_maxcut.rs index 11d964e..673f923 100644 --- a/src/rules/spinglass_maxcut.rs +++ b/src/rules/spinglass_maxcut.rs @@ -5,6 +5,9 @@ use crate::models::graph::MaxCut; use crate::models::optimization::SpinGlass; +use crate::poly; +use crate::reduction; +use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::traits::Problem; use crate::types::ProblemSize; @@ -42,6 +45,16 @@ where } } +#[reduction( + source_graph = "SimpleGraph", + target_graph = "SimpleGraph", + overhead = { + ReductionOverhead::new(vec![ + ("num_spins", poly!(num_vertices)), + ("num_interactions", poly!(num_edges)), + ]) + } +)] impl ReduceTo> for MaxCut where W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, @@ -132,6 +145,16 @@ where } } +#[reduction( + source_graph = "SimpleGraph", + target_graph = "SimpleGraph", + overhead = { + ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_spins)), + ("num_edges", poly!(num_interactions)), + ]) + } +)] impl ReduceTo> for SpinGlass where W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, @@ -280,32 +303,3 @@ mod tests { } } -// Register reductions with inventory for auto-discovery -use crate::poly; -use crate::rules::registry::{ReductionEntry, ReductionOverhead}; - -inventory::submit! { - ReductionEntry { - source_name: "MaxCut", - target_name: "SpinGlass", - source_graph: "SimpleGraph", - target_graph: "SpinGlassGraph", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_spins", poly!(num_vertices)), - ("num_interactions", poly!(num_edges)), - ]), - } -} - -inventory::submit! { - ReductionEntry { - source_name: "SpinGlass", - target_name: "MaxCut", - source_graph: "SpinGlassGraph", - target_graph: "SimpleGraph", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_spins)), - ("num_edges", poly!(num_interactions)), - ]), - } -} diff --git a/src/rules/spinglass_qubo.rs b/src/rules/spinglass_qubo.rs index 8d70e24..dd67c90 100644 --- a/src/rules/spinglass_qubo.rs +++ b/src/rules/spinglass_qubo.rs @@ -6,6 +6,9 @@ //! Transformation: s = 2x - 1 (so x=0 → s=-1, x=1 → s=+1) use crate::models::optimization::{SpinGlass, QUBO}; +use crate::poly; +use crate::reduction; +use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::traits::Problem; use crate::types::ProblemSize; @@ -39,6 +42,14 @@ impl ReductionResult for ReductionQUBOToSG { } } +#[reduction( + target_graph = "SimpleGraph", + overhead = { + ReductionOverhead::new(vec![ + ("num_spins", poly!(num_vars)), + ]) + } +)] impl ReduceTo> for QUBO { type Result = ReductionQUBOToSG; @@ -121,6 +132,14 @@ impl ReductionResult for ReductionSGToQUBO { } } +#[reduction( + source_graph = "SimpleGraph", + overhead = { + ReductionOverhead::new(vec![ + ("num_vars", poly!(num_spins)), + ]) + } +)] impl ReduceTo> for SpinGlass { type Result = ReductionSGToQUBO; @@ -298,30 +317,3 @@ mod tests { } } -// Register reductions with inventory for auto-discovery -use crate::poly; -use crate::rules::registry::{ReductionEntry, ReductionOverhead}; - -inventory::submit! { - ReductionEntry { - source_name: "QUBO", - target_name: "SpinGlass", - source_graph: "QUBOMatrix", - target_graph: "SpinGlassGraph", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_spins", poly!(num_vars)), - ]), - } -} - -inventory::submit! { - ReductionEntry { - source_name: "SpinGlass", - target_name: "QUBO", - source_graph: "SpinGlassGraph", - target_graph: "QUBOMatrix", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_vars", poly!(num_spins)), - ]), - } -} diff --git a/src/rules/vertexcovering_independentset.rs b/src/rules/vertexcovering_independentset.rs index 70e2a08..1a26144 100644 --- a/src/rules/vertexcovering_independentset.rs +++ b/src/rules/vertexcovering_independentset.rs @@ -3,6 +3,9 @@ //! These problems are complements: a set S is an independent set iff V\S is a vertex cover. use crate::models::graph::{IndependentSet, VertexCovering}; +use crate::poly; +use crate::reduction; +use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::traits::Problem; use crate::types::ProblemSize; @@ -42,6 +45,16 @@ where } } +#[reduction( + source_graph = "SimpleGraph", + target_graph = "SimpleGraph", + overhead = { + ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_vertices)), + ("num_edges", poly!(num_edges)), + ]) + } +)] impl ReduceTo> for IndependentSet where W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, @@ -93,6 +106,16 @@ where } } +#[reduction( + source_graph = "SimpleGraph", + target_graph = "SimpleGraph", + overhead = { + ReductionOverhead::new(vec![ + ("num_vertices", poly!(num_vertices)), + ("num_edges", poly!(num_edges)), + ]) + } +)] impl ReduceTo> for VertexCovering where W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, @@ -207,32 +230,3 @@ mod tests { } } -// Register reductions with inventory for auto-discovery -use crate::poly; -use crate::rules::registry::{ReductionEntry, ReductionOverhead}; - -inventory::submit! { - ReductionEntry { - source_name: "IndependentSet", - target_name: "VertexCovering", - source_graph: "SimpleGraph", - target_graph: "SimpleGraph", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_vertices)), - ("num_edges", poly!(num_edges)), - ]), - } -} - -inventory::submit! { - ReductionEntry { - source_name: "VertexCovering", - target_name: "IndependentSet", - source_graph: "SimpleGraph", - target_graph: "SimpleGraph", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_vertices", poly!(num_vertices)), - ("num_edges", poly!(num_edges)), - ]), - } -} diff --git a/src/rules/vertexcovering_setcovering.rs b/src/rules/vertexcovering_setcovering.rs index 2f940ce..024b9f9 100644 --- a/src/rules/vertexcovering_setcovering.rs +++ b/src/rules/vertexcovering_setcovering.rs @@ -5,6 +5,9 @@ use crate::models::graph::VertexCovering; use crate::models::set::SetCovering; +use crate::poly; +use crate::reduction; +use crate::rules::registry::ReductionOverhead; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::traits::Problem; use crate::types::ProblemSize; @@ -44,6 +47,15 @@ where } } +#[reduction( + source_graph = "SimpleGraph", + overhead = { + ReductionOverhead::new(vec![ + ("num_sets", poly!(num_vertices)), + ("num_elements", poly!(num_edges)), + ]) + } +)] impl ReduceTo> for VertexCovering where W: Clone + Default + PartialOrd + Num + Zero + AddAssign + From + 'static, @@ -265,19 +277,3 @@ mod tests { } } -// Register reduction with inventory for auto-discovery -use crate::poly; -use crate::rules::registry::{ReductionEntry, ReductionOverhead}; - -inventory::submit! { - ReductionEntry { - source_name: "VertexCovering", - target_name: "SetCovering", - source_graph: "SimpleGraph", - target_graph: "SetSystem", - overhead_fn: || ReductionOverhead::new(vec![ - ("num_sets", poly!(num_vertices)), - ("num_elements", poly!(num_edges)), - ]), - } -} diff --git a/src/solvers/brute_force.rs b/src/solvers/brute_force.rs index f9955de..419ff74 100644 --- a/src/solvers/brute_force.rs +++ b/src/solvers/brute_force.rs @@ -178,7 +178,6 @@ impl BruteForceFloat for BruteForce { #[cfg(test)] mod tests { use super::*; - use crate::graph_types::SimpleGraph; use crate::types::{EnergyMode, ProblemSize}; // Simple maximization problem: maximize sum of selected weights @@ -189,8 +188,11 @@ mod tests { impl Problem for MaxSumProblem { const NAME: &'static str = "MaxSumProblem"; - type GraphType = SimpleGraph; - type Weight = i32; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "i32")] + } + type Size = i32; fn num_variables(&self) -> usize { @@ -227,8 +229,11 @@ mod tests { impl Problem for MinSumProblem { const NAME: &'static str = "MinSumProblem"; - type GraphType = SimpleGraph; - type Weight = i32; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "i32")] + } + type Size = i32; fn num_variables(&self) -> usize { @@ -265,8 +270,11 @@ mod tests { impl Problem for SelectAtMostOneProblem { const NAME: &'static str = "SelectAtMostOneProblem"; - type GraphType = SimpleGraph; - type Weight = i32; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "i32")] + } + type Size = i32; fn num_variables(&self) -> usize { @@ -296,6 +304,25 @@ mod tests { } } + #[test] + fn test_variant_for_test_problems() { + // Test that variant() works for all test problems + let v = MaxSumProblem::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0], ("graph", "SimpleGraph")); + assert_eq!(v[1], ("weight", "i32")); + + let v = MinSumProblem::variant(); + assert_eq!(v.len(), 2); + + let v = SelectAtMostOneProblem::variant(); + assert_eq!(v.len(), 2); + + let v = FloatProblem::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[1], ("weight", "f64")); + } + #[test] fn test_brute_force_maximization() { let problem = MaxSumProblem { @@ -402,8 +429,11 @@ mod tests { impl Problem for FloatProblem { const NAME: &'static str = "FloatProblem"; - type GraphType = SimpleGraph; - type Weight = f64; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "f64")] + } + type Size = f64; fn num_variables(&self) -> usize { @@ -457,8 +487,11 @@ mod tests { impl Problem for NearlyEqualProblem { const NAME: &'static str = "NearlyEqualProblem"; - type GraphType = SimpleGraph; - type Weight = f64; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "f64")] + } + type Size = f64; fn num_variables(&self) -> usize { @@ -493,6 +526,12 @@ mod tests { let best = solver.find_best_float(&problem); // Both should be considered optimal due to tolerance assert_eq!(best.len(), 2); + + // Test variant for NearlyEqualProblem + let v = NearlyEqualProblem::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0], ("graph", "SimpleGraph")); + assert_eq!(v[1], ("weight", "f64")); } #[test] diff --git a/src/traits.rs b/src/traits.rs index e563afe..458e216 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,8 +1,7 @@ //! Core traits for problem definitions. -use crate::graph_types::GraphMarker; use crate::types::{ - EnergyMode, LocalConstraint, LocalSolutionSize, NumericWeight, ProblemSize, SolutionSize, + EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, SolutionSize, }; use num_traits::{Num, Zero}; use std::ops::AddAssign; @@ -15,11 +14,10 @@ pub trait Problem: Clone { /// Base name of this problem type (e.g., "IndependentSet"). const NAME: &'static str; - /// The graph type this problem operates on. - type GraphType: GraphMarker; - - /// The weight type for this problem. - type Weight: NumericWeight; + /// Returns attributes describing this problem variant. + /// Each (key, value) pair describes a variant dimension. + /// Common keys: "graph", "weight" + fn variant() -> Vec<(&'static str, &'static str)>; /// The type used for objective/size values. type Size: Clone + PartialOrd + Num + Zero + AddAssign; @@ -120,7 +118,6 @@ pub fn csp_solution_size( #[cfg(test)] mod tests { use super::*; - use crate::graph_types::SimpleGraph; // A simple test problem: select binary variables to maximize sum of weights #[derive(Clone)] @@ -130,8 +127,14 @@ mod tests { impl Problem for SimpleWeightedProblem { const NAME: &'static str = "SimpleWeightedProblem"; - type GraphType = SimpleGraph; - type Weight = i32; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } + type Size = i32; fn num_variables(&self) -> usize { @@ -168,8 +171,14 @@ mod tests { impl Problem for SimpleCsp { const NAME: &'static str = "SimpleCsp"; - type GraphType = SimpleGraph; - type Weight = i32; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } + type Size = i32; fn num_variables(&self) -> usize { @@ -225,6 +234,25 @@ mod tests { } } + #[test] + fn test_variant_for_test_problems() { + // Test that variant() works for test problems + let v = SimpleWeightedProblem::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0], ("graph", "SimpleGraph")); + assert_eq!(v[1], ("weight", "i32")); + + let v = SimpleCsp::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0], ("graph", "SimpleGraph")); + assert_eq!(v[1], ("weight", "i32")); + + let v = MultiFlavorProblem::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0], ("graph", "SimpleGraph")); + assert_eq!(v[1], ("weight", "i32")); + } + #[test] fn test_simple_problem() { let problem = SimpleWeightedProblem { @@ -450,8 +478,14 @@ mod tests { impl Problem for MultiFlavorProblem { const NAME: &'static str = "MultiFlavorProblem"; - type GraphType = SimpleGraph; - type Weight = i32; + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![ + ("graph", "SimpleGraph"), + ("weight", "i32"), + ] + } + type Size = i32; fn num_variables(&self) -> usize { diff --git a/src/types.rs b/src/types.rs index 62f3102..7265ee9 100644 --- a/src/types.rs +++ b/src/types.rs @@ -25,6 +25,48 @@ impl NumericWeight for T where { } +/// Marker type for unweighted problems. +/// +/// Similar to Julia's `UnitWeight`, this type indicates that a problem +/// has uniform weights (all equal to 1). Used in the variant metadata system +/// to distinguish unweighted problem variants from weighted ones. +/// +/// Note: This type is primarily used as a marker in the `variant()` method +/// to indicate that a problem is unweighted. The actual weight type parameter +/// in problem structs is typically `i32` or similar numeric type, with +/// `"Unweighted"` appearing in the variant metadata. +/// +/// # Example +/// +/// ``` +/// use problemreductions::types::Unweighted; +/// +/// // In variant metadata, "Unweighted" indicates uniform weights: +/// // fn variant() -> Vec<(&'static str, &'static str)> { +/// // vec![("graph", "SimpleGraph"), ("weight", "Unweighted")] +/// // } +/// // +/// // Weighted problems use the concrete type name: +/// // fn variant() -> Vec<(&'static str, &'static str)> { +/// // vec![("graph", "SimpleGraph"), ("weight", "i32")] +/// // } +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] +pub struct Unweighted; + +impl Unweighted { + /// Returns 1 for any index (all weights are unit). + pub fn get(&self, _index: usize) -> i32 { + 1 + } +} + +impl std::fmt::Display for Unweighted { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Unweighted") + } +} + /// Specifies whether larger or smaller objective values are better. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum EnergyMode { @@ -255,6 +297,26 @@ impl LocalSolutionSize { mod tests { use super::*; + #[test] + fn test_unweighted() { + let uw = Unweighted; + // Test get() method + assert_eq!(uw.get(0), 1); + assert_eq!(uw.get(100), 1); + assert_eq!(uw.get(usize::MAX), 1); + + // Test Display + assert_eq!(format!("{}", uw), "Unweighted"); + + // Test Clone, Copy, Default + let uw2 = uw; + let _uw3 = uw2; // Copy works (no clone needed) + let _uw4: Unweighted = Default::default(); + + // Test PartialEq + assert_eq!(Unweighted, Unweighted); + } + #[test] fn test_energy_mode() { let max_mode = EnergyMode::LargerSizeIsBetter; diff --git a/src/variant.rs b/src/variant.rs new file mode 100644 index 0000000..3fd0a2b --- /dev/null +++ b/src/variant.rs @@ -0,0 +1,138 @@ +//! Variant attribute utilities. + +use std::any::type_name; + +/// Extract short type name from full path. +/// e.g., "problemreductions::graph_types::SimpleGraph" -> "SimpleGraph" +pub fn short_type_name() -> &'static str { + let full = type_name::(); + full.rsplit("::").next().unwrap_or(full) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_short_type_name_primitive() { + assert_eq!(short_type_name::(), "i32"); + assert_eq!(short_type_name::(), "f64"); + } + + #[test] + fn test_short_type_name_struct() { + struct MyStruct; + assert_eq!(short_type_name::(), "MyStruct"); + } + + #[test] + fn test_variant_for_problems() { + use crate::models::graph::{ + Coloring, DominatingSet, IndependentSet, Matching, MaxCut, MaximalIS, VertexCovering, + }; + use crate::models::optimization::{SpinGlass, QUBO}; + use crate::models::satisfiability::{KSatisfiability, Satisfiability}; + use crate::models::set::{SetCovering, SetPacking}; + use crate::models::specialized::{BicliqueCover, CircuitSAT, Factoring, PaintShop, BMF}; + use crate::traits::Problem; + + // Test IndependentSet variants + let v = IndependentSet::::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0].0, "graph"); + assert_eq!(v[0].1, "SimpleGraph"); + assert_eq!(v[1].0, "weight"); + assert_eq!(v[1].1, "i32"); + + let v = IndependentSet::::variant(); + assert_eq!(v[1].1, "f64"); + + // Test VertexCovering + let v = VertexCovering::::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0].1, "SimpleGraph"); + assert_eq!(v[1].1, "i32"); + + // Test DominatingSet + let v = DominatingSet::::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0].1, "SimpleGraph"); + + // Test Matching + let v = Matching::::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0].1, "SimpleGraph"); + + // Test MaxCut + let v = MaxCut::::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0].1, "SimpleGraph"); + + let v = MaxCut::::variant(); + assert_eq!(v[1].1, "f64"); + + // Test Coloring (no weight parameter) + let v = Coloring::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0].1, "SimpleGraph"); + + // Test MaximalIS (no weight parameter) + let v = MaximalIS::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0].1, "SimpleGraph"); + + // Test Satisfiability + let v = Satisfiability::::variant(); + assert_eq!(v.len(), 2); + + // Test KSatisfiability + let v = KSatisfiability::<3, i32>::variant(); + assert_eq!(v.len(), 2); + + // Test SetPacking + let v = SetPacking::::variant(); + assert_eq!(v.len(), 2); + + // Test SetCovering + let v = SetCovering::::variant(); + assert_eq!(v.len(), 2); + + // Test SpinGlass + let v = SpinGlass::::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[1].1, "f64"); + + let v = SpinGlass::::variant(); + assert_eq!(v[1].1, "i32"); + + // Test QUBO + let v = QUBO::::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[1].1, "f64"); + + // Test CircuitSAT + let v = CircuitSAT::::variant(); + assert_eq!(v.len(), 2); + + // Test Factoring (no type parameters) + let v = Factoring::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0].1, "SimpleGraph"); + assert_eq!(v[1].1, "i32"); + + // Test BicliqueCover (no type parameters) + let v = BicliqueCover::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0].1, "SimpleGraph"); + + // Test BMF (no type parameters) + let v = BMF::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0].1, "SimpleGraph"); + + // Test PaintShop (no type parameters) + let v = PaintShop::variant(); + assert_eq!(v.len(), 2); + assert_eq!(v[0].1, "SimpleGraph"); + } +}