Skip to content

Commit 2eb68fb

Browse files
GiggleLiuclaude
andauthored
Simplify variant system and clean up type hierarchy (#66)
* move paper static files * Simplify variant system: derive categories from module_path, use node indices in JSON - Replace hardcoded `categorize_type` and `compute_doc_path` with derivation from `ProblemSchemaEntry.module_path` via inventory lookup - Simplify reduction_graph.json edges to use node indices instead of full objects - Use `G::NAME` instead of `short_type_name::<G>()` in variant() for correct graph type names (e.g. "GridGraph" not "GridGraph<i32>") - Remove manual `register_types` function; use `Problem::NAME` directly - Add MIS GridGraph/UnitDiskGraph reductions via unitdiskmapping module - Update docs (introduction.md, reductions.typ) to dereference node indices Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: apply rustfmt formatting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove hardcoded fallback from #[reduction] macro, make ReduceTo impls concrete The proc macro had a fallback path for type-generic impls that silently hardcoded "SimpleGraph"/"Unweighted" defaults and used heuristics, producing wrong variants (e.g., KColoring lost "k" field, MaximumSetPacking got spurious "graph" field). Changes: - Macro: replace type-generic fallback with compile error, remove source_graph/target_graph/source_weighted/target_weighted attributes, delete dead helper functions - Make 10 ReduceTo impls concrete across 7 reduction files (i32 or f64) - MaximumSetPacking→QUBO: only f64 impl (i32 promotes via natural edge) - Add ConcreteVariantEntry registrations for Unweighted variants so natural weight-promotion edges work correctly Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add design doc: type system cleanup (WeightElement, One, SatisfactionProblem) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add implementation plan: type system cleanup (WeightElement, One, SatisfactionProblem) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add WeightElement trait and One type, remove Unweighted/Weights/NumericWeight Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: update graph problem impls to use WeightElement Replace verbose trait bounds (Clone + Default + PartialOrd + Num + Zero + AddAssign + 'static) with WeightElement in Problem and OptimizationProblem impls for all 8 graph problems. Use W::Sum as the metric/value type and W::to_sum() for weight accumulation. Also update 5 reduction rule files that reference these graph problems. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: update set and optimization problem impls to use WeightElement Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: update reduction rule bounds to use WeightElement Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: rename Unweighted to One in variant metadata Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add SatisfactionProblem marker trait Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: cleanup remaining Unweighted references in comments Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * update example * feat: add Triangular graph type and MIS reduction via triangular mapping Introduces a Triangular lattice graph type (subtype of UnitDiskGraph) and a reduction from MIS<SimpleGraph> to MIS<Triangular> using the weighted triangular unit disk mapping, with O(n²) overhead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve PR review comments and improve test coverage - Fix variantDefaults.weight from "Unweighted" to "One" in docs - Update design.md weight hierarchy docs to use "One" instead of "Unweighted" - Format variants.rs inventory::submit! as multi-line blocks - Apply rustfmt to sat_ksat.rs macro and other files - Add UnitDiskGraph → GridGraph reduction test for coverage - Add Triangular Graph trait method tests for coverage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: remove manual variant registration, infer natural edges only Remove ConcreteVariantEntry and variants.rs — reduction graph nodes now come exclusively from explicit #[reduction] registrations. Natural edges between same-name variants are still inferred from the subtype partial order on graph/weight types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: rustdoc warning for unclosed HTML tags and update CLAUDE.md - Wrap generic types in backticks in maximumsetpacking_qubo.rs doc comment - Update CLAUDE.md: Unweighted→One, add Triangular/WeightElement, fix variant ID docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add Triangular to design.md graph types and hierarchy - Add Triangular to graph types table and hierarchy diagram - Update variant discovery description to reflect automatic node inference Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add natural edge example for Triangular MIS reduction Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add natural edge test for MIS/Triangular → MIS/SimpleGraph Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add generic natural-edge reduction system Introduce GraphCast trait, ReductionAutoCast struct, and impl_natural_reduction! macro for declarative graph subtype relaxation reductions. First usage: MIS<Triangular> → MIS<SimpleGraph>. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: use Petersen graph and ILP solver for natural edge closed-loop test Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ee2494d commit 2eb68fb

81 files changed

Lines changed: 2353 additions & 2549 deletions

File tree

Some content is hidden

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

.claude/CLAUDE.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ enum Direction { Maximize, Minimize }
8181
- `ReductionResult` provides `target_problem()` and `extract_solution()`
8282
- `Solver::find_best()``Option<Vec<usize>>` for optimization problems; `Solver::find_satisfying()``Option<Vec<usize>>` for `Metric = bool`
8383
- `BruteForce::find_all_best()` / `find_all_satisfying()` return `Vec<Vec<usize>>` for all optimal/satisfying solutions
84-
- Graph types: SimpleGraph, GridGraph, UnitDiskGraph, Hypergraph
85-
- Weight types: `Unweighted` (marker), `i32`, `f64`
84+
- Graph types: SimpleGraph, GridGraph, UnitDiskGraph, Triangular, HyperGraph
85+
- Weight types: `One` (unit weight marker), `i32`, `f64` — all implement `WeightElement` trait
86+
- `WeightElement` trait: `type Sum: NumericSize` + `fn to_sum(&self)` — converts weight to a summable numeric type
8687
- Weight management via inherent methods (`weights()`, `set_weights()`, `is_weighted()`), not traits
8788
- `NumericSize` supertrait bundles common numeric bounds (`Clone + Default + PartialOrd + Num + Zero + Bounded + AddAssign + 'static`)
8889

@@ -93,11 +94,11 @@ Problem types use explicit optimization prefixes:
9394
- No prefix: `MaxCut`, `SpinGlass`, `QUBO`, `ILP`, `Satisfiability`, `KSatisfiability`, `CircuitSAT`, `Factoring`, `MaximalIS`, `PaintShop`, `BicliqueCover`, `BMF`
9495

9596
### Problem Variant IDs
96-
Reduction graph nodes use variant IDs: `ProblemName[/GraphType][/Weighted]`
97-
- Base: `MaximumIndependentSet` (SimpleGraph, unweighted)
98-
- Graph variant: `MaximumIndependentSet/GridGraph`
99-
- Weighted variant: `MaximumIndependentSet/Weighted`
100-
- Both: `MaximumIndependentSet/GridGraph/Weighted`
97+
Reduction graph nodes use variant key-value pairs from `Problem::variant()`:
98+
- Base: `MaximumIndependentSet` (empty variant = defaults)
99+
- Graph variant: `MaximumIndependentSet {graph: "GridGraph", weight: "i32"}`
100+
- Weight variant: `MaximumIndependentSet {graph: "SimpleGraph", weight: "f64"}`
101+
- Nodes come exclusively from `#[reduction]` registrations; natural edges between same-name variants are inferred from the graph/weight subtype partial order
101102

102103
## Conventions
103104

.claude/skills/issue-to-pr.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Present issue summary to user.
4949

5050
Check that the issue template is fully filled out:
5151
- For **[Model]** issues: A clear mathmatical definition, Type specification, Variables and fields, The complexity clarification, verify an existing solver can solve it, or a solving strategy is provided, A detailed example for human.
52-
- For **[Rule]** issues: Source, Target, Reference to verify information, Implementable reduction algorithm, Test dataset generation method, Size overhead, A clear example for human.
52+
- For **[Rule]** issues: Source, Target, Reference to verify information, Implementable reduction algorithm, Test dataset generation method, Size overhead, A reduction example for human to verify the reduction is correct. Please put a high standard on the example: it must be in tutorial style with clear intuition and is easy to understand.
5353

5454
Verify facts provided by the user, feel free to ask user questions. If any piece is missing or unclear, comment on the issue via `gh issue comment <number> --body "..."` to ask user clarify. Then stop and wait — do NOT proceed until the issue is complete.
5555

docs/paper/reductions.typ

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"CircuitSAT": [CircuitSAT],
4545
"Factoring": [Factoring],
4646
"GridGraph": [GridGraph MIS],
47+
"Triangular": [Triangular MIS],
4748
)
4849

4950
// Definition label: "def:<ProblemName>" — each definition block must have a matching label
@@ -60,15 +61,15 @@
6061
// Extract reductions for a problem from graph-data (returns (name, label) pairs)
6162
#let get-reductions-to(problem-name) = {
6263
graph-data.edges
63-
.filter(e => e.source.name == problem-name)
64-
.map(e => (name: e.target.name, lbl: reduction-label(e.source.name, e.target.name)))
64+
.filter(e => graph-data.nodes.at(e.source).name == problem-name)
65+
.map(e => (name: graph-data.nodes.at(e.target).name, lbl: reduction-label(graph-data.nodes.at(e.source).name, graph-data.nodes.at(e.target).name)))
6566
.dedup(key: e => e.name)
6667
}
6768

6869
#let get-reductions-from(problem-name) = {
6970
graph-data.edges
70-
.filter(e => e.target.name == problem-name)
71-
.map(e => (name: e.source.name, lbl: reduction-label(e.source.name, e.target.name)))
71+
.filter(e => graph-data.nodes.at(e.target).name == problem-name)
72+
.map(e => (name: graph-data.nodes.at(e.source).name, lbl: reduction-label(graph-data.nodes.at(e.source).name, graph-data.nodes.at(e.target).name)))
7273
.dedup(key: e => e.name)
7374
}
7475

@@ -166,9 +167,9 @@
166167

167168
// Find edge in graph-data by source/target names
168169
#let find-edge(source, target) = {
169-
let edge = graph-data.edges.find(e => e.source.name == source and e.target.name == target)
170+
let edge = graph-data.edges.find(e => graph-data.nodes.at(e.source).name == source and graph-data.nodes.at(e.target).name == target)
170171
if edge == none {
171-
edge = graph-data.edges.find(e => e.source.name == target and e.target.name == source)
172+
edge = graph-data.edges.find(e => graph-data.nodes.at(e.source).name == target and graph-data.nodes.at(e.target).name == source)
172173
}
173174
edge
174175
}
@@ -205,9 +206,9 @@
205206
) = {
206207
let arrow = sym.arrow.r
207208
let edge = find-edge(source, target)
208-
let src-disp = if edge != none { variant-display(edge.source) }
209+
let src-disp = if edge != none { variant-display(graph-data.nodes.at(edge.source)) }
209210
else { display-name.at(source) }
210-
let tgt-disp = if edge != none { variant-display(edge.target) }
211+
let tgt-disp = if edge != none { variant-display(graph-data.nodes.at(edge.target)) }
211212
else { display-name.at(target) }
212213
let src-lbl = label("def:" + source)
213214
let tgt-lbl = label("def:" + target)
@@ -851,10 +852,10 @@ The following reductions to Integer Linear Programming are straightforward formu
851852
*Example: Petersen Graph.*#footnote[Generated using `cargo run --example export_petersen_mapping` from the accompanying code repository.] The Petersen graph ($n=10$, MIS$=4$) maps to a $30 times 42$ King's subgraph with 219 nodes and overhead $Delta = 89$. Solving MIS on the grid yields $"MIS"(G_"grid") = 4 + 89 = 93$. The weighted and unweighted KSG mappings share identical grid topology (same node positions and edges); only the vertex weights differ. With triangular lattice encoding @nguyen2023, the same graph maps to a $42 times 60$ grid with 395 nodes and overhead $Delta = 375$, giving $"MIS"(G_"tri") = 4 + 375 = 379$.
852853

853854
// Load JSON data
854-
#let petersen = json("petersen_source.json")
855-
#let square_weighted = json("petersen_square_weighted.json")
856-
#let square_unweighted = json("petersen_square_unweighted.json")
857-
#let triangular_mapping = json("petersen_triangular.json")
855+
#let petersen = json("static/petersen_source.json")
856+
#let square_weighted = json("static/petersen_square_weighted.json")
857+
#let square_unweighted = json("static/petersen_square_unweighted.json")
858+
#let triangular_mapping = json("static/petersen_triangular.json")
858859

859860
#figure(
860861
grid(
@@ -884,6 +885,14 @@ The following reductions to Integer Linear Programming are straightforward formu
884885
caption: [Unit disk mappings of the Petersen graph. Blue: weight 1, red: weight 2, green: weight 3.],
885886
) <fig:petersen-mapping>
886887

888+
#reduction-rule("MaximumIndependentSet", "Triangular")[
889+
@nguyen2023 Any MIS problem on a general graph $G$ can be reduced to MIS on a weighted triangular lattice graph with at most quadratic overhead in the number of vertices.
890+
][
891+
_Construction._ Same copy-line method as the KSG mapping, but uses a triangular lattice instead of a square grid. Crossing and simplifier gadgets are adapted for triangular geometry, producing a unit disk graph on a triangular grid where edges connect nodes within unit distance under the triangular metric.
892+
893+
_Overhead._ Both vertex and edge counts grow as $O(n^2)$ where $n = |V|$, matching the KSG mapping.
894+
]
895+
887896
*Weighted Extension.* For MWIS, copy lines use weighted vertices (weights 1, 2, or 3). Source weights $< 1$ are added to designated "pin" vertices.
888897

889898
*QUBO Mapping.* A QUBO problem $min bold(x)^top Q bold(x)$ maps to weighted MIS on a grid by:
@@ -897,7 +906,7 @@ See #link("https://github.com/CodingThrust/problem-reductions/blob/main/examples
897906
#context {
898907
let covered = covered-rules.get()
899908
let json-edges = {
900-
let edges = graph-data.edges.map(e => (e.source.name, e.target.name))
909+
let edges = graph-data.edges.map(e => (graph-data.nodes.at(e.source).name, graph-data.nodes.at(e.target).name))
901910
let unique = ()
902911
for e in edges {
903912
if unique.find(u => u.at(0) == e.at(0) and u.at(1) == e.at(1)) == none {
File renamed without changes.
File renamed without changes.
File renamed without changes.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Type System Cleanup Design
2+
3+
## Problem
4+
5+
The weight and trait system has several mathematical inconsistencies:
6+
7+
1. **Weight dual role**: The type parameter `W` serves as both the per-element weight type and the accumulation/metric type. This prevents using a unit-weight type (`One`) because `One + One` can't produce `2` within the same type.
8+
9+
2. **Dead abstractions**: `Unweighted(usize)` is never used as a type parameter. The `Weights` trait is implemented but never used outside its own tests. `NumericWeight` and `NumericSize` are nearly identical traits.
10+
11+
3. **Missing satisfaction trait**: Satisfaction problems (SAT, CircuitSAT, KColoring, Factoring) use `Metric = bool` but have no shared trait. The `BruteForce::find_satisfying()` method uses `Problem<Metric = bool>` inline.
12+
13+
## Design
14+
15+
### 1. `WeightElement` trait + `One` type
16+
17+
Introduce a trait that maps weight element types to their accumulation type:
18+
19+
```rust
20+
/// Maps a weight element to its sum/metric type.
21+
pub trait WeightElement: Clone + Default + 'static {
22+
/// The numeric type used for sums and comparisons.
23+
type Sum: NumericSize;
24+
/// Convert this weight element to the sum type.
25+
fn to_sum(&self) -> Self::Sum;
26+
}
27+
```
28+
29+
Implementations:
30+
31+
```rust
32+
/// The constant 1. Unit weight for unweighted problems.
33+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
34+
pub struct One;
35+
36+
impl WeightElement for One {
37+
type Sum = i32;
38+
fn to_sum(&self) -> i32 { 1 }
39+
}
40+
41+
impl WeightElement for i32 {
42+
type Sum = i32;
43+
fn to_sum(&self) -> i32 { *self }
44+
}
45+
46+
impl WeightElement for f64 {
47+
type Sum = f64;
48+
fn to_sum(&self) -> f64 { *self }
49+
}
50+
```
51+
52+
**Impact on problems:**
53+
54+
Before:
55+
```rust
56+
impl<G, W> Problem for MaximumIndependentSet<G, W>
57+
where W: Clone + Default + PartialOrd + Num + Zero + AddAssign + 'static
58+
{
59+
type Metric = SolutionSize<W>;
60+
fn evaluate(&self, config: &[usize]) -> SolutionSize<W> {
61+
let mut total = W::zero();
62+
for (i, &sel) in config.iter().enumerate() {
63+
if sel == 1 { total += self.weights[i].clone(); }
64+
}
65+
SolutionSize::Valid(total)
66+
}
67+
}
68+
69+
impl<G, W> OptimizationProblem for MaximumIndependentSet<G, W> {
70+
type Value = W;
71+
}
72+
```
73+
74+
After:
75+
```rust
76+
impl<G, W: WeightElement> Problem for MaximumIndependentSet<G, W>
77+
where W::Sum: PartialOrd
78+
{
79+
type Metric = SolutionSize<W::Sum>;
80+
fn evaluate(&self, config: &[usize]) -> SolutionSize<W::Sum> {
81+
let mut total = W::Sum::zero();
82+
for (i, &sel) in config.iter().enumerate() {
83+
if sel == 1 { total += self.weights[i].to_sum(); }
84+
}
85+
SolutionSize::Valid(total)
86+
}
87+
}
88+
89+
impl<G, W: WeightElement> OptimizationProblem for MaximumIndependentSet<G, W> {
90+
type Value = W::Sum;
91+
}
92+
```
93+
94+
**Variant output:** `variant()` uses `short_type_name::<W>()` which returns `"One"`, `"i32"`, or `"f64"`. The variant label changes from `"Unweighted"` to `"One"`.
95+
96+
### 2. `SatisfactionProblem` marker trait
97+
98+
```rust
99+
/// Marker trait for satisfaction (decision) problems.
100+
pub trait SatisfactionProblem: Problem<Metric = bool> {}
101+
```
102+
103+
Implemented by: `Satisfiability`, `KSatisfiability`, `CircuitSAT`, `KColoring`, `Factoring`.
104+
105+
No new methods. Makes the problem category explicit in the type system. `BruteForce::find_satisfying()` can use `P: SatisfactionProblem` as its bound.
106+
107+
### 3. Merge `NumericWeight` / `NumericSize`
108+
109+
Delete `NumericWeight`. Keep `NumericSize` as the sole numeric bound trait:
110+
111+
```rust
112+
pub trait NumericSize:
113+
Clone + Default + PartialOrd + Num + Zero + Bounded + AddAssign + 'static
114+
{}
115+
```
116+
117+
This is the bound on `WeightElement::Sum`. The extra `Bounded` requirement (vs the old `NumericWeight`) is needed for solver penalty calculations and is satisfied by `i32` and `f64`.
118+
119+
### Removals
120+
121+
- `Unweighted` struct (replaced by `One`)
122+
- `Weights` trait (unused, subsumed by `WeightElement`)
123+
- `NumericWeight` trait (merged into `NumericSize`)
124+
125+
### Reduction impact
126+
127+
Concrete `ReduceTo` impls change `Unweighted` references to `One`. The `ConcreteVariantEntry` registrations in `variants.rs` change `"Unweighted"` to `"One"`. The natural edge system (weight subtype hierarchy) adds `One` as a subtype of `i32`.
128+
129+
### Variant impact
130+
131+
The `variant()` output for unweighted problems changes from `("weight", "Unweighted")` to `("weight", "One")`. The reduction graph JSON, paper, and JavaScript visualization update accordingly.

0 commit comments

Comments
 (0)