Skip to content

Commit 148ef7f

Browse files
GiggleLiuclaude
andauthored
Refactor: address KISS and DRY violations (#70) (#71)
* refactor: trim MaximumIndependentSet API — remove delegation methods Remove delegation methods (num_vertices, num_edges, edges, has_edge, set_weights, from_graph_unit_weights) from MaximumIndependentSet so callers go through .graph() directly. Rename weights_ref() to weights() returning &[W] instead of &Vec<W>. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: trim MinimumVertexCover API — remove delegation methods Remove num_vertices(), num_edges(), edges(), has_edge(), set_weights(), from_graph_unit_weights(), and cloning weights() from MinimumVertexCover. Rename weights_ref() to weights() returning &[W]. Callers now access graph topology via .graph() and get weights by reference. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: trim MaximumClique API — remove delegation methods Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: trim MaximalIS API — remove delegation methods Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: trim MinimumDominatingSet API — remove delegation methods Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: extract classify_problem_category from to_json() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: extract filter_redundant_base_nodes from to_json() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: extract is_natural_edge from to_json() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: implement BipartiteGraph with standard bipartite representation Replace the ZST marker in graph_types.rs with a real BipartiteGraph implementation in src/topology/ that stores left/right partition sizes and edges in bipartite-local coordinates. The Graph trait maps to a unified vertex space where right vertices are offset by left_size. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: expand "Chaining Reductions" with ResolvedPath example Allow KSatisfiability<KN> construction by skipping clause-length validation when K::K is None, enabling the natural K3→KN widening step. Replace the short chaining example in getting-started.md with a richer 3-SAT→SAT→MIS pipeline that demonstrates ReductionGraph path planning, variant casts, and ILPSolver extraction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: implement PlanarGraph as validated SimpleGraph wrapper Replace the ZST marker PlanarGraph in graph_types.rs with a real topology type that wraps SimpleGraph and validates the necessary planarity condition |E| <= 3|V| - 6 for |V| >= 3. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: apply rustfmt formatting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: polish design.md and update diagrams Rewrite design.md with clearer structure: add variant system section, lattice diagrams, and improved trait hierarchy visuals. Update Makefile diagram target to support --root flag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove plan files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use correct module path in test_classify_problem_category The test used "models::sat" but the actual module is "models::satisfiability". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a4264a6 commit 148ef7f

78 files changed

Lines changed: 3552 additions & 1299 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.

Makefile

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,14 @@ doc:
5656
cp -r target/doc docs/book/api
5757

5858
# Generate SVG diagrams from Typst sources (light + dark themes)
59-
TYPST_DIAGRAMS := $(wildcard docs/src/static/*.typ)
59+
TYPST_DOC_DIAGRAMS := $(wildcard docs/src/static/*.typ)
60+
TYPST_PAPER_DIAGRAMS := $(wildcard docs/paper/static/*.typ)
6061
diagrams:
61-
@for src in $(TYPST_DIAGRAMS); do \
62+
@for src in $(TYPST_DOC_DIAGRAMS); do \
6263
base=$$(basename $$src .typ); \
63-
echo "Compiling $$base..."; \
64-
typst compile $$src --input dark=false docs/src/static/$$base.svg; \
65-
typst compile $$src --input dark=true docs/src/static/$$base-dark.svg; \
64+
echo "Compiling $$base (doc)..."; \
65+
typst compile $$src --root=. --input dark=false docs/src/static/$$base.svg; \
66+
typst compile $$src --root=. --input dark=true docs/src/static/$$base-dark.svg; \
6667
done
6768

6869
# Build and serve mdBook with API docs

docs/src/design.md

Lines changed: 258 additions & 185 deletions
Large diffs are not rendered by default.

docs/src/getting-started.md

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,25 +55,71 @@ assert!(metric.is_valid());
5555

5656
### Chaining Reductions
5757

58-
Reductions can be chained. Each step preserves the solution mapping:
58+
Reductions compose into multi-step chains. A `ResolvedPath` describes the plan —
59+
each step carries the problem name and variant, each edge is either a `Reduction`
60+
(with overhead) or a `NaturalCast` (free subtype relaxation).
61+
Here we solve a 3-SAT formula by chaining through Satisfiability
62+
and MaximumIndependentSet:
5963

6064
```rust
65+
use std::collections::BTreeMap;
6166
use problemreductions::prelude::*;
6267
use problemreductions::topology::SimpleGraph;
68+
use problemreductions::rules::{ReductionGraph, EdgeKind};
69+
use problemreductions::solvers::ILPSolver;
6370

64-
// SetPacking -> IndependentSet -> VertexCover
65-
let sp = MaximumSetPacking::<i32>::new(vec![vec![0, 1], vec![1, 2], vec![2, 3]]);
71+
// --- Plan: obtain a ResolvedPath ---
6672

67-
let r1 = ReduceTo::<MaximumIndependentSet<SimpleGraph, i32>>::reduce_to(&sp);
68-
let r2 = ReduceTo::<MinimumVertexCover<SimpleGraph, i32>>::reduce_to(r1.target_problem());
73+
let graph = ReductionGraph::new();
74+
let path = graph.find_shortest_path_by_name("KSatisfiability", "MaximumIndependentSet").unwrap();
75+
let source = BTreeMap::from([("k".to_string(), "K3".to_string())]);
76+
let resolved = graph.resolve_path(&path, &source, &BTreeMap::new()).unwrap();
6977

70-
// Solve final target, extract back through chain
71-
let solver = BruteForce::new();
72-
let vc_sol = solver.find_best(r2.target_problem()).unwrap();
73-
let is_sol = r2.extract_solution(&vc_sol);
74-
let sp_sol = r1.extract_solution(&is_sol);
78+
// The resolved path:
79+
// step 0: KSatisfiability {k: "K3"}
80+
// step 1: Satisfiability {}
81+
// step 2: MaximumIndependentSet {graph: "SimpleGraph", weight: "i32"}
82+
// edge 0: Reduction (K3-SAT → SAT, trivial embedding)
83+
// edge 1: Reduction (SAT → MIS, Karp 1972)
84+
85+
// --- Execute: create, reduce, solve, extract ---
86+
87+
// Create: 3-SAT formula (a∨b∨¬c)∧(¬a∨¬b∨¬c)∧(¬a∨b∨c)∧(a∨¬b∨c)
88+
let ksat = KSatisfiability::<K3>::new(3, vec![
89+
CNFClause::new(vec![1, 2, -3]), // a ∨ b ∨ ¬c
90+
CNFClause::new(vec![-1, -2, -3]), // ¬a ∨ ¬b ∨ ¬c
91+
CNFClause::new(vec![-1, 2, 3]), // ¬a ∨ b ∨ c
92+
CNFClause::new(vec![1, -2, 3]), // a ∨ ¬b ∨ c
93+
]);
94+
95+
// Widen: 3-SAT → N-SAT (natural variant cast, KN accepts any clause size)
96+
let nsat = KSatisfiability::<KN>::new(ksat.num_vars(), ksat.clauses().to_vec());
97+
98+
// Reduce: N-SAT → Satisfiability (trivial embedding)
99+
let r1 = ReduceTo::<Satisfiability>::reduce_to(&nsat);
100+
101+
// Reduce: Satisfiability → MaximumIndependentSet (Karp reduction)
102+
let r2 = ReduceTo::<MaximumIndependentSet<SimpleGraph, i32>>::reduce_to(r1.target_problem());
103+
104+
// Solve: MIS via ILP (internally: MIS → ILP → solve → extract)
105+
let ilp = ILPSolver::new();
106+
let mis_solution = ilp.solve_reduced(r2.target_problem()).unwrap();
107+
108+
// Extract: trace back through the reduction chain
109+
let sat_solution = r2.extract_solution(&mis_solution);
110+
let nsat_solution = r1.extract_solution(&sat_solution);
111+
112+
// Verify: satisfies the original 3-SAT formula
113+
assert!(ksat.evaluate(&nsat_solution));
75114
```
76115

116+
The `ILPSolver::solve_reduced()` handles the final MIS → ILP reduction,
117+
solve, and extraction internally. The caller traces back the explicit chain
118+
with `extract_solution()` at each step, recovering a satisfying assignment
119+
for the original formula.
120+
121+
> **Note:** `ILPSolver` requires the `ilp` feature flag (see [Solvers](#solvers)).
122+
77123
## Solvers
78124

79125
Two solvers for testing purposes are available:

docs/src/static/lattices-dark.svg

Lines changed: 687 additions & 0 deletions
Loading

docs/src/static/lattices.svg

Lines changed: 687 additions & 0 deletions
Loading

docs/src/static/lattices.typ

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// Demonstration of graph types used in the problem-reductions library.
2+
// Compile:
3+
// typst compile lattices.typ --input dark=false lattices.svg
4+
// typst compile lattices.typ --input dark=true lattices-dark.svg
5+
#import "@preview/cetz:0.4.2": canvas, draw
6+
#import "../../paper/lib.typ": g-node, g-edge
7+
8+
#set page(width: auto, height: auto, margin: 12pt, fill: none)
9+
10+
#let lattices(dark: false) = {
11+
// ── Theme colors ────────────────────────────────────────────────
12+
let (fg, edge-color, secondary) = if dark {
13+
(rgb("#e2e8f0"), rgb("#94a3b8"), rgb("#94a3b8"))
14+
} else {
15+
(rgb("#1e293b"), rgb("#64748b"), rgb("#64748b"))
16+
}
17+
18+
let (node-fill, node-highlight) = if dark {
19+
(rgb("#1e3a5f"), rgb("#2563eb"))
20+
} else {
21+
(rgb("#dbeafe"), rgb("#93c5fd"))
22+
}
23+
24+
let hyper-colors = if dark {
25+
(
26+
(fill: rgb("#1e3a5f").transparentize(30%), stroke: rgb("#60a5fa")),
27+
(fill: rgb("#7f1d1d").transparentize(30%), stroke: rgb("#f87171")),
28+
(fill: rgb("#064e3b").transparentize(30%), stroke: rgb("#34d399")),
29+
)
30+
} else {
31+
(
32+
(fill: rgb("#dbeafe").transparentize(40%), stroke: rgb("#4e79a7")),
33+
(fill: rgb("#fecaca").transparentize(40%), stroke: rgb("#e15759")),
34+
(fill: rgb("#d1fae5").transparentize(40%), stroke: rgb("#059669")),
35+
)
36+
}
37+
38+
let hyper-node-fill = if dark { rgb("#1e293b") } else { white }
39+
40+
let disk-fill = if dark {
41+
rgb("#1e3a5f").transparentize(70%)
42+
} else {
43+
rgb("#dbeafe").transparentize(70%)
44+
}
45+
46+
set text(fill: fg, size: 9pt)
47+
48+
// ── (a) SimpleGraph ──────────────────────────────────────────────
49+
let simple-graph-fig() = {
50+
import draw: *
51+
let vs = ((0, 0), (2, 0), (0, 1.5), (2, 1.5), (1, 2.5))
52+
let es = ((0,1),(0,2),(1,3),(2,3),(2,4),(3,4))
53+
for (u, v) in es { g-edge(vs.at(u), vs.at(v), stroke: 1pt + edge-color) }
54+
for (k, pos) in vs.enumerate() {
55+
g-node(pos, name: "s" + str(k), fill: node-fill, stroke: 0.5pt + edge-color, label: str(k))
56+
}
57+
}
58+
59+
// ── (b) HyperGraph ──────────────────────────────────────────────
60+
let hypergraph-fig() = {
61+
import draw: *
62+
let vs = ((0, 0), (1.5, 0.3), (2.5, 0), (0.5, 1.5), (2, 1.5), (1.2, 2.5))
63+
64+
// Hyperedge A: {0, 1, 3}
65+
draw.hobby(
66+
(-.3, -.3), (0.8, -.2), (1.9, 0.2), (0.8, 1.8), (-.2, 1.8), (-.5, 0.5),
67+
close: true,
68+
fill: hyper-colors.at(0).fill,
69+
stroke: 0.8pt + hyper-colors.at(0).stroke,
70+
)
71+
// Hyperedge B: {1, 2, 4}
72+
draw.hobby(
73+
(1.1, -.2), (2.8, -.3), (2.6, 1.8), (1.6, 1.8), (0.9, 0.8),
74+
close: true,
75+
fill: hyper-colors.at(1).fill,
76+
stroke: 0.8pt + hyper-colors.at(1).stroke,
77+
)
78+
// Hyperedge C: {3, 4, 5}
79+
draw.hobby(
80+
(0.1, 1.2), (1.6, 1.0), (2.4, 1.8), (1.5, 2.9), (0.1, 2.2),
81+
close: true,
82+
fill: hyper-colors.at(2).fill,
83+
stroke: 0.8pt + hyper-colors.at(2).stroke,
84+
)
85+
86+
for (k, pos) in vs.enumerate() {
87+
g-node(pos, name: "h" + str(k), fill: hyper-node-fill, stroke: 1pt + edge-color, label: str(k))
88+
}
89+
}
90+
91+
// ── (c) UnitDiskGraph ────────────────────────────────────────────
92+
let unit-disk-fig() = {
93+
import draw: *
94+
let vs = ((0.2, 0.2), (1.0, 0.0), (2.2, 0.3), (0.0, 1.2), (1.2, 1.5), (2.0, 1.1), (0.8, 2.3))
95+
let r = 1.25
96+
97+
// Radius disk around vertex 4
98+
draw.circle(vs.at(4), radius: r, fill: disk-fill, stroke: (dash: "dashed", paint: edge-color, thickness: 0.6pt))
99+
100+
// Compute edges: connect pairs within distance r
101+
let es = ()
102+
for i in range(vs.len()) {
103+
for j in range(i + 1, vs.len()) {
104+
let dx = vs.at(i).at(0) - vs.at(j).at(0)
105+
let dy = vs.at(i).at(1) - vs.at(j).at(1)
106+
if calc.sqrt(dx * dx + dy * dy) <= r {
107+
es.push((i, j))
108+
}
109+
}
110+
}
111+
112+
for (u, v) in es { g-edge(vs.at(u), vs.at(v), stroke: 0.8pt + edge-color) }
113+
for (k, pos) in vs.enumerate() {
114+
let fill = if k == 4 { node-highlight } else { node-fill }
115+
g-node(pos, name: "u" + str(k), fill: fill, stroke: 0.5pt + edge-color, label: str(k))
116+
}
117+
118+
// Radius label
119+
draw.content((vs.at(4).at(0) + r + 0.15, vs.at(4).at(1) + 0.1), text(7pt, fill: secondary)[$r$])
120+
}
121+
122+
// ── (d) KingsSubgraph ────────────────────────────────────────────
123+
let kings-fig() = {
124+
import draw: *
125+
let rows = 4
126+
let cols = 5
127+
let sp = 0.6
128+
let vs = ()
129+
for row in range(rows) {
130+
for col in range(cols) {
131+
vs.push((col * sp, -row * sp))
132+
}
133+
}
134+
135+
let es = ()
136+
for row in range(rows) {
137+
for col in range(cols) {
138+
let i = row * cols + col
139+
if col + 1 < cols { es.push((i, i + 1)) }
140+
if row + 1 < rows { es.push((i, i + cols)) }
141+
if row + 1 < rows and col + 1 < cols { es.push((i, i + cols + 1)) }
142+
if row + 1 < rows and col > 0 { es.push((i, i + cols - 1)) }
143+
}
144+
}
145+
146+
for (u, v) in es { g-edge(vs.at(u), vs.at(v), stroke: 0.6pt + edge-color) }
147+
for (k, pos) in vs.enumerate() {
148+
g-node(pos, name: "k" + str(k), radius: 0.12, fill: node-fill, stroke: 0.5pt + edge-color)
149+
}
150+
}
151+
152+
// ── (e) TriangularSubgraph ───────────────────────────────────────
153+
let triangular-fig() = {
154+
import draw: *
155+
let rows = 5
156+
let cols = 6
157+
let sp = 0.6
158+
let sqrt3_2 = calc.sqrt(3) / 2
159+
let vs = ()
160+
for row in range(rows) {
161+
let offset = if calc.rem(row, 2) == 0 { 0 } else { 0.5 * sp }
162+
for col in range(cols) {
163+
vs.push((col * sp + offset, -row * sqrt3_2 * sp))
164+
}
165+
}
166+
167+
let es = ()
168+
for row in range(rows) {
169+
for col in range(cols) {
170+
let i = row * cols + col
171+
if col + 1 < cols { es.push((i, i + 1)) }
172+
if row + 1 < rows {
173+
if calc.rem(row, 2) == 0 {
174+
if col > 0 { es.push((i, (row + 1) * cols + col - 1)) }
175+
es.push((i, (row + 1) * cols + col))
176+
} else {
177+
es.push((i, (row + 1) * cols + col))
178+
if col + 1 < cols { es.push((i, (row + 1) * cols + col + 1)) }
179+
}
180+
}
181+
}
182+
}
183+
184+
for (u, v) in es { g-edge(vs.at(u), vs.at(v), stroke: 0.5pt + edge-color) }
185+
for (k, pos) in vs.enumerate() {
186+
g-node(pos, name: "t" + str(k), radius: 0.1, fill: node-fill, stroke: 0.4pt + edge-color)
187+
}
188+
}
189+
190+
// ── Layout ───────────────────────────────────────────────────────
191+
let font-size = 12pt
192+
canvas({
193+
import draw: *
194+
simple-graph-fig()
195+
content((1, -1), text(font-size, [(a) SimpleGraph]))
196+
set-origin((5, 0))
197+
hypergraph-fig()
198+
content((1, -1), text(font-size, [(b) HyperGraph]))
199+
set-origin((5, 0))
200+
unit-disk-fig()
201+
content((1, -1), text(font-size, [(c) UnitDiskGraph]))
202+
set-origin((-10, -2))
203+
kings-fig()
204+
content((1, -2.6), text(font-size, [(d) KingsSubgraph]))
205+
set-origin((5, 0))
206+
triangular-fig()
207+
content((1, -2.6), text(font-size, [(e) TriangularSubgraph]))
208+
})
209+
}
210+
211+
#let standalone-dark = sys.inputs.at("dark", default: "false") == "true"
212+
#lattices(dark: standalone-dark)

0 commit comments

Comments
 (0)