Skip to content

Commit eb92e0a

Browse files
GiggleLiuclaude
andauthored
Rewrite getting-started with Factoring→SpinGlass and path overhead API (#78)
* feat: rewrite getting-started with Factoring→SpinGlass example and path overhead API - Rewrite getting-started.md Example 2 to Factoring→SpinGlass with notebook style - Add path_overheads() as single source of truth for per-edge overhead lookup - Refactor compose_path_overhead() and tests to build on path_overheads() (DRY) - Remove evaluate_path_overhead() in favor of symbolic per-edge checks - Add 7 rich paper examples recovered from stash (SAT→KColoring, SAT→DS, etc.) - Fix rustdoc unresolved link to ProblemSize - Add force-push prohibition to CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add coverage for ReductionPath and ReductionStep Display impls Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5be5149 commit eb92e0a

8 files changed

Lines changed: 399 additions & 149 deletions

File tree

.claude/CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ make release V=x.y.z # Tag and push a new release (CI publishes to crates.io)
3535
make test clippy # Must pass before PR
3636
```
3737

38+
## Git Safety
39+
- **NEVER force push** (`git push --force`, `git push -f`, `git push --force-with-lease`). This is an absolute rule with no exceptions. Force push can silently destroy other people's work and stashed changes.
40+
3841
## Architecture
3942

4043
### Core Modules

docs/paper/reductions.typ

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,18 @@ where $P$ is a penalty weight large enough that any constraint violation costs m
635635
_Solution extraction._ For $v_(j,i) in S$ with literal $x_k$: set $x_k = 1$; for $overline(x_k)$: set $x_k = 0$.
636636
]
637637

638-
#reduction-rule("Satisfiability", "KColoring")[
638+
#let sat_kc = load-example("satisfiability_to_kcoloring")
639+
#let sat_kc_r = load-results("satisfiability_to_kcoloring")
640+
#let sat_kc_sol = sat_kc_r.solutions.at(0)
641+
#reduction-rule("Satisfiability", "KColoring",
642+
example: true,
643+
example-caption: [5-variable SAT with 3 unit clauses to 3-coloring],
644+
extra: [
645+
SAT assignment: $(x_1, ..., x_5) = (#sat_kc_sol.source_config.map(str).join(", "))$ \
646+
Construction: 3 base + $2 times #sat_kc.source.instance.num_vars$ variable gadgets + OR-gadgets $arrow.r$ #sat_kc.target.instance.num_vertices vertices, #sat_kc.target.instance.num_edges edges \
647+
#sat_kc_r.solutions.len() valid 3-colorings (color symmetry of satisfying assignments) #sym.checkmark
648+
],
649+
)[
639650
@garey1979 Given CNF $phi$, construct graph $G$ such that $phi$ is satisfiable iff $G$ is 3-colorable.
640651
][
641652
_Construction._ (1) Base triangle: TRUE, FALSE, AUX vertices with all pairs connected. (2) Variable gadget for $x_i$: vertices $"pos"_i$, $"neg"_i$ connected to each other and to AUX. (3) Clause gadget: for $(ell_1 or ... or ell_k)$, apply OR-gadgets iteratively producing output $o$, then connect $o$ to FALSE and AUX.
@@ -645,7 +656,18 @@ where $P$ is a penalty weight large enough that any constraint violation costs m
645656
_Solution extraction._ Set $x_i = 1$ iff $"color"("pos"_i) = "color"("TRUE")$.
646657
]
647658

648-
#reduction-rule("Satisfiability", "MinimumDominatingSet")[
659+
#let sat_ds = load-example("satisfiability_to_minimumdominatingset")
660+
#let sat_ds_r = load-results("satisfiability_to_minimumdominatingset")
661+
#let sat_ds_sol = sat_ds_r.solutions.at(0)
662+
#reduction-rule("Satisfiability", "MinimumDominatingSet",
663+
example: true,
664+
example-caption: [5-variable 7-clause 3-SAT to dominating set],
665+
extra: [
666+
SAT assignment: $(x_1, ..., x_5) = (#sat_ds_sol.source_config.map(str).join(", "))$ \
667+
Vertex structure: $#sat_ds.target.instance.num_vertices = 3 times #sat_ds.source.instance.num_vars + #sat_ds.source.instance.num_clauses$ (variable triangles + clause vertices) \
668+
Dominating set of size $n = #sat_ds.source.instance.num_vars$: one vertex per variable triangle #sym.checkmark
669+
],
670+
)[
649671
@garey1979 Given CNF $phi$ with $n$ variables and $m$ clauses, $phi$ is satisfiable iff the constructed graph has a dominating set of size $n$.
650672
][
651673
_Construction._ (1) Variable triangle for $x_i$: vertices $"pos"_i = 3i$, $"neg"_i = 3i+1$, $"dum"_i = 3i+2$ forming a triangle. (2) Clause vertex $c_j = 3n+j$ connected to $"pos"_i$ if $x_i in C_j$, to $"neg"_i$ if $overline(x_i) in C_j$.
@@ -661,7 +683,18 @@ where $P$ is a penalty weight large enough that any constraint violation costs m
661683
_Variable mapping:_ Identity — variables and clauses unchanged. _Solution extraction:_ identity.
662684
]
663685

664-
#reduction-rule("Satisfiability", "KSatisfiability")[
686+
#let sat_ksat = load-example("satisfiability_to_ksatisfiability")
687+
#let sat_ksat_r = load-results("satisfiability_to_ksatisfiability")
688+
#let sat_ksat_sol = sat_ksat_r.solutions.at(0)
689+
#reduction-rule("Satisfiability", "KSatisfiability",
690+
example: true,
691+
example-caption: [Mixed-size clauses (sizes 1 to 5) to 3-SAT],
692+
extra: [
693+
Source: #sat_ksat.source.instance.num_vars variables, #sat_ksat.source.instance.num_clauses clauses (sizes 1, 2, 3, 3, 4, 5) \
694+
Target 3-SAT: $#sat_ksat.target.instance.num_vars = #sat_ksat.source.instance.num_vars + 7$ variables, #sat_ksat.target.instance.num_clauses clauses (small padded, large split) \
695+
First solution: $(x_1, ..., x_5) = (#sat_ksat_sol.source_config.map(str).join(", "))$, auxiliary vars are don't-cares #sym.checkmark
696+
],
697+
)[
665698
@cook1971 @garey1979 Any SAT formula converts to $k$-SAT ($k >= 3$) preserving satisfiability.
666699
][
667700
_Small clauses ($|C| < k$):_ Pad $(ell_1 or ... or ell_r)$ with auxiliary $y$: $(ell_1 or ... or ell_r or y or overline(y) or ...)$ to length $k$.
@@ -672,7 +705,17 @@ where $P$ is a penalty weight large enough that any constraint violation costs m
672705
_Correctness._ Original clause true $arrow.l.r$ auxiliary chain can propagate truth through new clauses.
673706
]
674707

675-
#reduction-rule("CircuitSAT", "SpinGlass")[
708+
#let cs_sg = load-example("circuitsat_to_spinglass")
709+
#let cs_sg_r = load-results("circuitsat_to_spinglass")
710+
#reduction-rule("CircuitSAT", "SpinGlass",
711+
example: true,
712+
example-caption: [1-bit full adder to Ising model],
713+
extra: [
714+
Circuit: #cs_sg.source.instance.num_gates gates (2 XOR, 2 AND, 1 OR), #cs_sg.source.instance.num_variables variables \
715+
Target: #cs_sg.target.instance.num_spins spins (each gate allocates I/O + auxiliary spins) \
716+
#cs_sg_r.solutions.len() ground states ($= 2^3$ valid input combinations for the full adder) #sym.checkmark
717+
],
718+
)[
676719
@whitfield2012 @lucas2014 Each gate maps to a gadget whose ground states encode valid I/O.
677720
][
678721
_Spin mapping:_ $sigma in {0,1} arrow.bar s = 2sigma - 1 in {-1, +1}$.
@@ -694,7 +737,26 @@ where $P$ is a penalty weight large enough that any constraint violation costs m
694737
caption: [Ising gadgets for logic gates. Ground states match truth tables.]
695738
) <tab:gadgets>
696739

697-
#reduction-rule("Factoring", "CircuitSAT")[
740+
#let fact_cs = load-example("factoring_to_circuitsat")
741+
#let fact_cs_r = load-results("factoring_to_circuitsat")
742+
#let fact-decode(config, start, count) = {
743+
let pow2 = (1, 2, 4, 8, 16, 32)
744+
range(count).fold(0, (acc, i) => acc + config.at(start + i) * pow2.at(i))
745+
}
746+
#let fact-nbf = fact_cs.source.instance.num_bits_first
747+
#let fact-nbs = fact_cs.source.instance.num_bits_second
748+
#reduction-rule("Factoring", "CircuitSAT",
749+
example: true,
750+
example-caption: [Factor $N = #fact_cs.source.instance.number$],
751+
extra: [
752+
Circuit: $#fact-nbf times #fact-nbs$ array multiplier with #fact_cs.target.instance.num_gates gates, #fact_cs.target.instance.num_variables variables \
753+
#fact_cs_r.solutions.len() solutions: #fact_cs_r.solutions.map(sol => {
754+
let p = fact-decode(sol.source_config, 0, fact-nbf)
755+
let q = fact-decode(sol.source_config, fact-nbf, fact-nbs)
756+
$#p times #q = #fact_cs.source.instance.number$
757+
}).join(" and ") #sym.checkmark
758+
],
759+
)[
698760
An array multiplier with output constrained to $N$ is satisfiable iff $N$ factors within bit bounds. _(Folklore; no canonical reference.)_
699761
][
700762
_Construction._ Build $m times n$ array multiplier for $p times q$:
@@ -708,13 +770,36 @@ where $P$ is a penalty weight large enough that any constraint violation costs m
708770
_Solution extraction._ $p = sum_i p_i 2^(i-1)$, $q = sum_j q_j 2^(j-1)$.
709771
]
710772

711-
#reduction-rule("MaxCut", "SpinGlass")[
773+
#let mc_sg = load-example("maxcut_to_spinglass")
774+
#let mc_sg_r = load-results("maxcut_to_spinglass")
775+
#let mc_sg_sol = mc_sg_r.solutions.at(0)
776+
#let mc_sg_cut = mc_sg.source.instance.edges.filter(e => mc_sg_sol.source_config.at(e.at(0)) != mc_sg_sol.source_config.at(e.at(1))).len()
777+
#reduction-rule("MaxCut", "SpinGlass",
778+
example: true,
779+
example-caption: [Petersen graph ($n = 10$, unit weights) to Ising],
780+
extra: [
781+
Direct 1:1 mapping: vertices $arrow.r$ spins, $J_(i j) = w_(i j) = 1$, $h_i = 0$ \
782+
Partition: $S = {#mc_sg_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(", ")}$ vs $overline(S) = {#mc_sg_sol.source_config.enumerate().filter(((i, x)) => x == 0).map(((i, x)) => str(i)).join(", ")}$ \
783+
Cut value $= #mc_sg_cut$ ($#mc_sg_r.solutions.len()$-fold degenerate) #sym.checkmark
784+
],
785+
)[
712786
@barahona1982 Set $J_(i j) = w_(i j)$, $h_i = 0$. Maximizing cut equals minimizing $-sum J_(i j) s_i s_j$.
713787
][
714788
Opposite-partition vertices satisfy $s_i s_j = -1$, contributing $-J_(i j)(-1) = J_(i j)$ to the energy. _Variable mapping:_ $J_(i j) = w_(i j)$, $h_i = 0$, spins $s_i = 2 sigma_i - 1$ where $sigma_i in {0, 1}$ is the partition label. _Solution extraction:_ partition $= {i : s_i = +1}$.
715789
]
716790

717-
#reduction-rule("SpinGlass", "MaxCut")[
791+
#let sg_mc = load-example("spinglass_to_maxcut")
792+
#let sg_mc_r = load-results("spinglass_to_maxcut")
793+
#let sg_mc_sol = sg_mc_r.solutions.at(0)
794+
#reduction-rule("SpinGlass", "MaxCut",
795+
example: true,
796+
example-caption: [10-spin Ising with alternating $J_(i j) in {plus.minus 1}$],
797+
extra: [
798+
All $h_i = 0$: no ancilla needed, direct 1:1 vertex mapping \
799+
Edge weights $w_(i j) = J_(i j) in {plus.minus 1}$ (alternating couplings) \
800+
Ground state ($#sg_mc_r.solutions.len()$-fold degenerate): partition $S = {#sg_mc_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(", ")}$ #sym.checkmark
801+
],
802+
)[
718803
@barahona1982 @lucas2014 Ground states of Ising models correspond to maximum cuts.
719804
][
720805
_MaxCut $arrow.r$ SpinGlass:_ Set $J_(i j) = w_(i j)$, $h_i = 0$. Maximizing cut equals minimizing $-sum J_(i j) s_i s_j$ since $s_i s_j = -1$ when $s_i != s_j$.

docs/src/getting-started.md

Lines changed: 129 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,66 +28,164 @@ The core workflow is: **create** a problem, **reduce** it to a target, **solve**
2828

2929
</div>
3030

31-
### Complete Example
31+
### Example 1: Direct reduction
3232

33-
```rust
33+
Reduce Maximum Independent Set to Minimum Vertex Cover on a 4-vertex path
34+
graph, solve the target, and extract the solution back.
35+
36+
#### Step 1 — Create the source problem
37+
38+
A path graph `0–1–2–3` has 4 vertices and 3 edges.
39+
40+
```rust,ignore
3441
use problemreductions::prelude::*;
3542
use problemreductions::topology::SimpleGraph;
3643
37-
// 1. Create: Independent Set on a path graph (4 vertices)
38-
let problem = MaximumIndependentSet::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), vec![1i32; 4]);
44+
let problem = MaximumIndependentSet::new(
45+
SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]),
46+
vec![1i32; 4],
47+
);
48+
```
49+
50+
#### Step 2 — Reduce to Minimum Vertex Cover
51+
52+
`ReduceTo` applies a single-step reduction. The result holds the target
53+
problem and knows how to map solutions back.
3954

40-
// 2. Reduce: Transform to Minimum Vertex Cover
55+
```rust,ignore
56+
println!("Source: {} {:?}", MaximumIndependentSet::<SimpleGraph, i32>::NAME,
57+
MaximumIndependentSet::<SimpleGraph, i32>::variant());
4158
let reduction = ReduceTo::<MinimumVertexCover<SimpleGraph, i32>>::reduce_to(&problem);
4259
let target = reduction.target_problem();
60+
println!("Target: {} {:?}, {} variables",
61+
MinimumVertexCover::<SimpleGraph, i32>::NAME,
62+
MinimumVertexCover::<SimpleGraph, i32>::variant(),
63+
target.num_variables());
64+
```
65+
66+
```text
67+
Source: MaximumIndependentSet [("graph", "SimpleGraph"), ("weight", "i32")]
68+
Target: MinimumVertexCover [("graph", "SimpleGraph"), ("weight", "i32")], 4 variables
69+
```
70+
71+
#### Step 3 — Solve the target problem
72+
73+
`BruteForce` enumerates all configurations and returns the optimal one.
4374

44-
// 3. Solve: Find optimal solution to the target problem
75+
```rust,ignore
4576
let solver = BruteForce::new();
4677
let target_solution = solver.find_best(target).unwrap();
78+
println!("VC solution: {:?}", target_solution);
79+
```
4780

48-
// 4. Extract: Map solution back to original problem
49-
let solution = reduction.extract_solution(&target_solution);
81+
```text
82+
VC solution: [1, 0, 1, 0]
83+
```
84+
85+
#### Step 4 — Extract and verify
86+
87+
`extract_solution` maps the Vertex Cover solution back to an Independent Set
88+
solution by complementing the configuration.
5089

51-
// Verify: solution is valid for the original problem
90+
```rust,ignore
91+
let solution = reduction.extract_solution(&target_solution);
5292
let metric = problem.evaluate(&solution);
93+
println!("IS solution: {:?} -> size {:?}", solution, metric);
5394
assert!(metric.is_valid());
5495
```
5596

56-
### Chaining Reductions
97+
```text
98+
IS solution: [0, 1, 0, 1] -> size Valid(2)
99+
```
100+
101+
S ⊆ V is an independent set iff V \ S is a vertex cover, so the complement
102+
maps optimality in one direction to optimality in the other.
103+
104+
### Example 2: Reduction path search — integer factoring to spin glass
105+
106+
Real-world problems often require **chaining** multiple reductions. Here we factor the integer 6 by reducing `Factoring` through the reduction graph to `SpinGlass`, through automatic reduction path search.
107+
108+
```rust,ignore
109+
{{#include ../../examples/chained_reduction_factoring_to_spinglass.rs:imports}}
110+
111+
{{#include ../../examples/chained_reduction_factoring_to_spinglass.rs:example}}
112+
```
113+
114+
Let's walk through each step.
57115

58-
Reductions compose into multi-step chains. The `ReductionGraph` discovers
59-
paths through the variant-level graph. Find a `ReductionPath` first, then
60-
convert it to a typed `ExecutablePath<S, T>` via `make_executable()`. Call
61-
`reduce()` once and get a `ChainedReduction` with `target_problem()` and
62-
`extract_solution()`, just like a single-step reduction.
116+
#### Step 1 — Discover the reduction path
63117

64-
Here we solve a 3-SAT formula by chaining through Satisfiability
65-
and MaximumIndependentSet:
118+
`ReductionGraph` holds every registered reduction. `find_cheapest_path`
119+
searches for the shortest chain from a source problem variant to a target
120+
variant.
66121

67122
```rust,ignore
68-
{{#include ../../examples/chained_reduction_ksat_to_mis.rs:imports}}
123+
{{#include ../../examples/chained_reduction_factoring_to_spinglass.rs:step1}}
124+
```
69125

70-
{{#include ../../examples/chained_reduction_ksat_to_mis.rs:example}}
126+
```text
127+
Factoring → CircuitSAT → SpinGlass {graph: "SimpleGraph", weight: "i32"}
128+
```
129+
130+
#### Step 2 — Create the Factoring problem
131+
132+
`Factoring::new(m, n, target)` creates a factoring instance: find two factors
133+
`p` (m-bit) and `q` (n-bit) such that `p × q = target`. Here we factor **6**
134+
with two 2-bit factors, expecting **2 × 3** or **3 × 2**.
71135

72-
{{#include ../../examples/chained_reduction_ksat_to_mis.rs:overhead}}
136+
```rust,ignore
137+
{{#include ../../examples/chained_reduction_factoring_to_spinglass.rs:step2}}
73138
```
74139

75-
The `ExecutablePath` handles variant casts (e.g., `K3``KN`) and
76-
cross-problem reductions (e.g., SAT → MIS) uniformly. The `ChainedReduction`
77-
extracts solutions back through the entire chain in one call.
140+
#### Step 3 — Solve with ILPSolver
78141

79-
`compose_path_overhead` composes the per-step polynomial overheads into a
80-
symbolic formula mapping source variables to final target variables:
142+
`solve_reduced` reduces the problem to ILP internally and solves it in one
143+
call. It returns a configuration vector for the original problem — no manual
144+
extraction needed. For small instances you can also use `BruteForce`, but
145+
`ILPSolver` scales to much larger problems.
146+
147+
```rust,ignore
148+
{{#include ../../examples/chained_reduction_factoring_to_spinglass.rs:step3}}
149+
```
150+
151+
#### Step 4 — Read and verify the factors
152+
153+
`read_factors` decodes the binary configuration back into the two integer
154+
factors.
155+
156+
```rust,ignore
157+
{{#include ../../examples/chained_reduction_factoring_to_spinglass.rs:step4}}
158+
```
81159

82160
```text
83-
num_vertices = num_literals
84-
num_edges = num_literals^2
161+
6 = 3 × 2
85162
```
86163

87-
This result comes from composing two steps: KSatisfiability → Satisfiability
88-
is identity (same size fields), then Satisfiability → MIS maps
89-
`num_vertices = num_literals` and `num_edges = num_literals²`.
90-
Substituting the identity through gives the final polynomials above.
164+
#### Step 5 — Inspect the overhead
165+
166+
Each reduction edge carries a polynomial overhead mapping source problem
167+
sizes to target sizes. `path_overheads` returns the per-edge
168+
polynomials, and `compose_path_overhead` composes them symbolically into a
169+
single end-to-end formula.
170+
171+
```rust,ignore
172+
{{#include ../../examples/chained_reduction_factoring_to_spinglass.rs:overhead}}
173+
```
174+
175+
```text
176+
Factoring → CircuitSAT:
177+
num_variables = num_bits_first * num_bits_second
178+
num_assignments = num_bits_first * num_bits_second
179+
CircuitSAT → SpinGlass {graph: "SimpleGraph", weight: "i32"}:
180+
num_spins = num_assignments
181+
num_interactions = num_assignments
182+
SpinGlass {graph: "SimpleGraph", weight: "i32"} → SpinGlass {graph: "SimpleGraph", weight: "f64"}:
183+
num_spins = num_spins
184+
num_interactions = num_interactions
185+
Composed (source → target):
186+
num_spins = num_bits_first * num_bits_second
187+
num_interactions = num_bits_first * num_bits_second
188+
```
91189

92190
## Solvers
93191

0 commit comments

Comments
 (0)