diff --git a/.claude/rules/typst-drawing.md b/.claude/rules/typst-drawing.md new file mode 100644 index 00000000..628366e3 --- /dev/null +++ b/.claude/rules/typst-drawing.md @@ -0,0 +1,91 @@ +--- +description: Use when editing Typst files — covers general patterns, CeTZ drawing, plotting, and utility functions +globs: ["*.typ"] +--- + +# Typst Editing Reference + +Reference files: ~/Documents/private-note/notes/typst-learn/ (typst-tricks.typ, typst-drawing.typ, typst-my-utils.typ) + +## General Typst Patterns + +### Page Setup +- Standalone figures: `#set page(width: auto, height: auto, margin: 5pt)` +- Standard notes: `#set page(margin: 2cm)` + `#set text(size: 10pt)` + `#set heading(numbering: "1.1.")` +- Numbered equations: `#set math.equation(numbering: "(1)")` + +### Common Packages +- CeTZ: `@preview/cetz:0.4.0`, `@preview/cetz-plot:0.1.2` +- Algorithms: `@preview/algorithmic:1.0.3` — `Function`, `For`, `While`, `If`, `ElseIf`, `Else`, `Assign`, `Return`, `Comment` +- Math: `@preview/physica:0.9.3` (physics notation), `@preview/ouset:0.2.0` (over/under sets) +- Theorems: `@preview/ctheorems:1.1.2` or `@preview/unequivocal-ams:0.1.2` +- Random: `@preview/suiji:0.3.0` — `gen-rng(seed)`, `uniform(rng, size: n)`, `shuffle(rng, arr)` + +### Math Notation +- Bra-ket: `$|psi chevron.r$`, `$chevron.l phi|$` +- Blackboard bold: `$bb(I)$`, calligraphic: `$cal(E)$` +- Accents: `$hat(N)$`, `$tilde(H)$`, `$overline(X)$` +- Cases: `$ f(x) = cases(x^2 &"if" x > 0, 0 &"otherwise") $` +- Matrices with ellipses: `$mat(a, dots; dots.v, dots.down;)$` + +### Citations +- Inline: `@Author2024`, with locator: `@Author2024[Ch. 4]` +- Prose: `#cite(, form: "prose")` +- Compact slides style: `#set cite(style: "author-journal-year.csl")` + +### Functional Idioms +- `range(n).map(_ => 0)` — zeros array +- `a.zip(b).map(((x, y)) => ...)` — pairwise ops +- `for (k, (i, j)) in pts.enumerate() { ... }` — destructuring enumerate +- `dict.at(key, default: 0)` — dict with default + +### Content Helpers +- Infobox: `rect(stroke: color, inset: 8pt, radius: 4pt, width: 100%, [*Title:*\ body])` +- Inline image alignment: `box(image(...), baseline: (size - 20pt) / 2 + offset)` +- Image clipping: `box(clip: true, img, inset: (top: -top, bottom: -bottom, ...))` +- Two columns: `grid(columns: (1fr, 1fr), gutter: 20pt, left, right)` + +## CeTZ Drawing + +### Core Rules +1. **Name all objects** that will be referenced later: `circle(..., name: "c")`, `line(..., name: "edge")` +2. **Connect objects by name + anchor**, never by raw coordinates: `line("a.east", "b.west")`, not `line((2, 0), (5, 0))` +3. **Use `set-origin`** for sub-figures instead of manual coordinate offsets +4. **Use `on-layer`** for layering: -1 for backgrounds, 0 for main content, 1 for labels +5. **Use `content()` with `frame: "rect"`** for labeled boxes; use `fill: white, stroke: none` for edge labels +6. **Inside CeTZ functions**, always `import draw: *` for unqualified access + +### Gotchas +- **`arc`**: first parameter is the **start point** of the arc, not the center of the circle +- **`bezier`**: first two args are **start and end points**; remaining args are control points +- **Arrows**: prefer `mark: (end: "straight")` style — do NOT use `">"` +- **Stroke dict**: use `(paint: color, thickness: 1pt, dash: "dashed")` — NOT `stroke(...)` constructor + +### Quick Reference +- Shapes: `circle`, `rect`, `line`, `arc`, `bezier`, `hobby`, `catmull`, `merge-path`, `grid` +- Anchors: `"name.north"`, `.south`, `.east`, `.west`, `.center`, `.start`, `.mid`, `.end` +- Coordinates: `(x, y)`, `(rel: (dx, dy), to: "name")`, `("a", 50%, "b")`, `("a", "|-", "b")` +- Marks (arrows): `"straight"`, `">"`, `"stealth"`, `"|"`, `"o"`, `"<>"`, `"hook"`, `"]"` +- Strokes: `(dash: "dashed")`, `(dash: "dotted")`, `(dash: "dash-dotted")`, `2pt + red` +- Colors: `blue.lighten(60%)`, `green.darken(20%)`, `rgb("#f0f0fe")` +- Decorations: `decorations.brace`, `.flat-brace`, `.zigzag`, `.wave`, `.coil` +- Trees: `tree.tree((...), direction: "down", grow: 1.5, spread: 1.8)` + +### Drawing Patterns +- **Graph rendering**: name vertices as `str(k)`, connect with `line(str(k), str(l))` +- **Circular layout**: use `vrotate(v, theta)` helper to place vertices on a circle +- **Edge labels**: `content("edge.mid", label, fill: white, frame: "rect", padding: 0.08, stroke: none)` +- **Data-driven diagrams**: store layout as list of tuples, iterate with `for` loops +- **Tensor networks**: `tensor` (circle + label), `deltatensor` (small filled dot), `labeledge` (line + midpoint label) +- **Intersections**: `intersections("ix", { ...shapes... })` then reference `"ix.0"`, `"ix.1"` + +## CeTZ Plotting +- `plot.plot(size: (w, h), axis-style: "scientific", x-tick-step: 1, y-tick-step: 2, { ... })` +- Line: `plot.add(domain: (a, b), x => f(x), label: $f$, style: (stroke: blue))` +- Data: `plot.add(data, mark: "o", line: "spline")` — line types: `"linear"`, `"spline"`, `"vh"`, `"hv"` +- Scatter marks: `"*"`, `"o"`, `"square"`, `"triangle"`, `"+"`, `"|"`, `"-"`, `"<>"` +- Fill between: `plot.add-fill-between(f, g, domain: (a, b))` +- Reference lines: `plot.add-hline(y)`, `plot.add-vline(x)` +- Annotations: `plot.add-anchor("name", (x, y))`, `plot.annotate({ ... })` +- Bar chart: `chart.barchart(data, size: ..., mode: "clustered", labels: (...))` +- Pie chart: `chart.piechart(data, inner-radius: 0.5, outer-label: (content: auto, radius: 130%))` diff --git a/Makefile b/Makefile index 85e4e5b2..bc59f80c 100644 --- a/Makefile +++ b/Makefile @@ -153,6 +153,7 @@ cli: GRAPHS := diamond bull house petersen MODES := unweighted weighted triangular rust-export: + @mkdir -p tests/julia @for graph in $(GRAPHS); do \ for mode in $(MODES); do \ echo "Exporting $$graph ($$mode)..."; \ diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 07112ff0..9460a804 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -50,6 +50,7 @@ "PaintShop": [Paint Shop], "BicliqueCover": [Biclique Cover], "BinPacking": [Bin Packing], + "ClosestVectorProblem": [Closest Vector Problem], ) // Definition label: "def:" — each definition block must have a matching label @@ -637,6 +638,49 @@ Integer Linear Programming is a universal modeling framework: virtually every NP ) ] +#problem-def("ClosestVectorProblem")[ + Given a lattice basis $bold(B) in RR^(m times n)$ (columns $bold(b)_1, dots, bold(b)_n in RR^m$ spanning lattice $cal(L)(bold(B)) = {bold(B) bold(x) : bold(x) in ZZ^n}$) and target $bold(t) in RR^m$, find $bold(x) in ZZ^n$ minimizing $norm(bold(B) bold(x) - bold(t))_2$. +][ + The Closest Vector Problem is a fundamental lattice problem, proven NP-hard by van Emde Boas @vanemde1981. CVP appears in lattice-based cryptography, coding theory, and integer programming @lenstra1983. Kannan's enumeration algorithm @kannan1987 solves CVP in $n^(O(n))$ time; Micciancio and Voulgaris @micciancio2010 improved this to deterministic $O^*(4^n)$ using Voronoi cell computations, and Aggarwal, Dadush, and Stephens-Davidowitz @aggarwal2015 achieved randomized $O^*(2^n)$. + + *Example.* Consider the 2D lattice with basis $bold(b)_1 = (2, 0)^top$, $bold(b)_2 = (1, 2)^top$ and target $bold(t) = (2.8, 1.5)^top$. The lattice points near $bold(t)$ include $bold(B)(1, 0)^top = (2, 0)^top$, $bold(B)(0, 1)^top = (1, 2)^top$, and $bold(B)(1, 1)^top = (3, 2)^top$. The closest is $bold(B)(1, 1)^top = (3, 2)^top$ with distance $norm(bold(B)(1,1)^top - bold(t))_2 = norm((0.2, 0.5))_2 = sqrt(0.04 + 0.25) approx 0.539$. + + #figure( + canvas(length: 0.8cm, { + import draw: * + // Lattice points: B*(x1,x2) = x1*(2,0) + x2*(1,2) + for x1 in range(0, 3) { + for x2 in range(0, 3) { + let px = x1 * 2 + x2 * 1 + let py = x2 * 2 + let is-closest = (x1 == 1 and x2 == 1) + let nm = "p" + str(x1) + str(x2) + circle( + (px, py), + radius: if is-closest { 0.15 } else { 0.08 }, + fill: if is-closest { graph-colors.at(0) } else { luma(180) }, + stroke: if is-closest { 0.8pt + graph-colors.at(0) } else { 0.4pt + luma(120) }, + name: nm, + ) + } + } + // Target vector + circle((2.8, 1.5), radius: 0.1, fill: graph-colors.at(1), stroke: none, name: "target") + content((rel: (0, -0.45), to: "target"), text(7pt)[$bold(t)$]) + // Dashed line from target to closest point + line("target", "p11", stroke: (paint: graph-colors.at(0), thickness: 0.8pt, dash: "dashed")) + // Basis vectors as arrows from origin + line("p00", "p10", mark: (end: "straight"), stroke: 0.8pt + luma(100), name: "b1") + content((rel: (0, -0.35), to: "b1.mid"), text(7pt)[$bold(b)_1$]) + line("p00", "p01", mark: (end: "straight"), stroke: 0.8pt + luma(100), name: "b2") + content((rel: (-0.3, 0), to: "b2.mid"), text(7pt)[$bold(b)_2$]) + // Label closest point + content((rel: (0.45, 0.3), to: "p11"), text(7pt)[$bold(B)(1,1)^top$]) + }), + caption: [2D lattice with basis $bold(b)_1 = (2, 0)^top$, $bold(b)_2 = (1, 2)^top$. Target $bold(t) = (2.8, 1.5)^top$ (red) and closest lattice point $bold(B)(1,1)^top = (3, 2)^top$ (blue). Distance $norm(bold(B)(1,1)^top - bold(t))_2 approx 0.539$.], + ) +] + == Satisfiability Problems #problem-def("Satisfiability")[ diff --git a/docs/paper/references.bib b/docs/paper/references.bib index e8c6ff5d..ff74bef1 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -307,6 +307,43 @@ @article{epping2004 doi = {10.1016/S0166-218X(03)00442-6} } +@techreport{vanemde1981, + author = {Peter van Emde Boas}, + title = {Another NP-complete Problem and the Complexity of Computing Short Vectors in a Lattice}, + institution = {Mathematisch Instituut, Universiteit van Amsterdam}, + number = {81-04}, + year = {1981} +} + +@article{kannan1987, + author = {Ravi Kannan}, + title = {Minkowski's Convex Body Theorem and Integer Programming}, + journal = {Mathematics of Operations Research}, + volume = {12}, + number = {3}, + pages = {415--440}, + year = {1987}, + doi = {10.1287/moor.12.3.415} +} + +@inproceedings{micciancio2010, + author = {Daniele Micciancio and Panagiotis Voulgaris}, + title = {A Deterministic Single Exponential Time Algorithm for Most Lattice Problems Based on {V}oronoi Cell Computations}, + booktitle = {Proceedings of the 42nd ACM Symposium on Theory of Computing (STOC)}, + pages = {351--358}, + year = {2010}, + doi = {10.1145/1806689.1806739} +} + +@inproceedings{aggarwal2015, + author = {Divesh Aggarwal and Daniel Dadush and Noah Stephens-Davidowitz}, + title = {Solving the Closest Vector Problem in $2^n$ Time -- The Discrete {G}aussian Strikes Again!}, + booktitle = {Proceedings of the 56th IEEE Symposium on Foundations of Computer Science (FOCS)}, + pages = {563--580}, + year = {2015}, + doi = {10.1109/FOCS.2015.41} +} + @article{shannon1956, author = {Claude E. Shannon}, title = {The zero error capacity of a noisy channel}, diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index f3d9102c..8cc8d2ae 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -83,6 +83,27 @@ } ] }, + { + "name": "ClosestVectorProblem", + "description": "Find the closest lattice point to a target vector", + "fields": [ + { + "name": "basis", + "type_name": "Vec>", + "description": "Basis matrix B as column vectors" + }, + { + "name": "target", + "type_name": "Vec", + "description": "Target vector t" + }, + { + "name": "bounds", + "type_name": "Vec", + "description": "Integer bounds per variable" + } + ] + }, { "name": "Factoring", "description": "Factor a composite integer into two factors", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index 6d507247..7606b7fd 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -25,6 +25,24 @@ "doc_path": "models/specialized/struct.CircuitSAT.html", "complexity": "2^num_inputs" }, + { + "name": "ClosestVectorProblem", + "variant": { + "weight": "f64" + }, + "category": "optimization", + "doc_path": "models/optimization/struct.ClosestVectorProblem.html", + "complexity": "exp(num_basis_vectors)" + }, + { + "name": "ClosestVectorProblem", + "variant": { + "weight": "i32" + }, + "category": "optimization", + "doc_path": "models/optimization/struct.ClosestVectorProblem.html", + "complexity": "exp(num_basis_vectors)" + }, { "name": "Factoring", "variant": {}, @@ -332,7 +350,7 @@ "edges": [ { "source": 2, - "target": 4, + "target": 6, "overhead": [ { "field": "num_vars", @@ -347,7 +365,7 @@ }, { "source": 2, - "target": 33, + "target": 35, "overhead": [ { "field": "num_spins", @@ -361,7 +379,7 @@ "doc_path": "rules/circuit_spinglass/index.html" }, { - "source": 3, + "source": 5, "target": 2, "overhead": [ { @@ -376,8 +394,8 @@ "doc_path": "rules/factoring_circuit/index.html" }, { - "source": 3, - "target": 4, + "source": 5, + "target": 6, "overhead": [ { "field": "num_vars", @@ -391,8 +409,8 @@ "doc_path": "rules/factoring_ilp/index.html" }, { - "source": 4, - "target": 30, + "source": 6, + "target": 32, "overhead": [ { "field": "num_vars", @@ -402,8 +420,8 @@ "doc_path": "rules/ilp_qubo/index.html" }, { - "source": 6, - "target": 9, + "source": 8, + "target": 11, "overhead": [ { "field": "num_vertices", @@ -417,8 +435,8 @@ "doc_path": "rules/kcoloring_casts/index.html" }, { - "source": 9, - "target": 4, + "source": 11, + "target": 6, "overhead": [ { "field": "num_vars", @@ -432,8 +450,8 @@ "doc_path": "rules/coloring_ilp/index.html" }, { - "source": 9, - "target": 30, + "source": 11, + "target": 32, "overhead": [ { "field": "num_vars", @@ -443,8 +461,8 @@ "doc_path": "rules/coloring_qubo/index.html" }, { - "source": 10, - "target": 12, + "source": 12, + "target": 14, "overhead": [ { "field": "num_vars", @@ -458,8 +476,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 10, - "target": 30, + "source": 12, + "target": 32, "overhead": [ { "field": "num_vars", @@ -469,8 +487,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 10, - "target": 31, + "source": 12, + "target": 33, "overhead": [ { "field": "num_clauses", @@ -488,8 +506,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 11, - "target": 12, + "source": 13, + "target": 14, "overhead": [ { "field": "num_vars", @@ -503,8 +521,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 11, - "target": 30, + "source": 13, + "target": 32, "overhead": [ { "field": "num_vars", @@ -514,8 +532,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 11, - "target": 31, + "source": 13, + "target": 33, "overhead": [ { "field": "num_clauses", @@ -533,8 +551,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 12, - "target": 31, + "source": 14, + "target": 33, "overhead": [ { "field": "num_clauses", @@ -552,8 +570,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 13, - "target": 33, + "source": 15, + "target": 35, "overhead": [ { "field": "num_spins", @@ -567,8 +585,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 15, - "target": 4, + "source": 17, + "target": 6, "overhead": [ { "field": "num_vars", @@ -582,8 +600,8 @@ "doc_path": "rules/maximumclique_ilp/index.html" }, { - "source": 16, - "target": 17, + "source": 18, + "target": 19, "overhead": [ { "field": "num_vertices", @@ -597,8 +615,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 16, - "target": 21, + "source": 18, + "target": 23, "overhead": [ { "field": "num_vertices", @@ -612,8 +630,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 17, - "target": 22, + "source": 19, + "target": 24, "overhead": [ { "field": "num_vertices", @@ -627,8 +645,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 18, - "target": 16, + "source": 20, + "target": 18, "overhead": [ { "field": "num_vertices", @@ -642,8 +660,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 18, - "target": 17, + "source": 20, + "target": 19, "overhead": [ { "field": "num_vertices", @@ -657,8 +675,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 18, - "target": 19, + "source": 20, + "target": 21, "overhead": [ { "field": "num_vertices", @@ -672,8 +690,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 18, - "target": 20, + "source": 20, + "target": 22, "overhead": [ { "field": "num_vertices", @@ -687,8 +705,8 @@ "doc_path": "rules/maximumindependentset_triangular/index.html" }, { - "source": 18, - "target": 24, + "source": 20, + "target": 26, "overhead": [ { "field": "num_sets", @@ -702,8 +720,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 19, - "target": 4, + "source": 21, + "target": 6, "overhead": [ { "field": "num_vars", @@ -717,8 +735,8 @@ "doc_path": "rules/maximumindependentset_ilp/index.html" }, { - "source": 19, - "target": 26, + "source": 21, + "target": 28, "overhead": [ { "field": "num_sets", @@ -732,8 +750,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 19, - "target": 29, + "source": 21, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -747,8 +765,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 19, - "target": 30, + "source": 21, + "target": 32, "overhead": [ { "field": "num_vars", @@ -758,8 +776,8 @@ "doc_path": "rules/maximumindependentset_qubo/index.html" }, { - "source": 20, - "target": 22, + "source": 22, + "target": 24, "overhead": [ { "field": "num_vertices", @@ -773,8 +791,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 21, - "target": 18, + "source": 23, + "target": 20, "overhead": [ { "field": "num_vertices", @@ -788,8 +806,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 21, - "target": 22, + "source": 23, + "target": 24, "overhead": [ { "field": "num_vertices", @@ -803,8 +821,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 22, - "target": 19, + "source": 24, + "target": 21, "overhead": [ { "field": "num_vertices", @@ -818,8 +836,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 23, - "target": 4, + "source": 25, + "target": 6, "overhead": [ { "field": "num_vars", @@ -833,8 +851,8 @@ "doc_path": "rules/maximummatching_ilp/index.html" }, { - "source": 23, - "target": 26, + "source": 25, + "target": 28, "overhead": [ { "field": "num_sets", @@ -848,8 +866,8 @@ "doc_path": "rules/maximummatching_maximumsetpacking/index.html" }, { - "source": 24, - "target": 18, + "source": 26, + "target": 20, "overhead": [ { "field": "num_vertices", @@ -863,8 +881,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 24, - "target": 26, + "source": 26, + "target": 28, "overhead": [ { "field": "num_sets", @@ -878,8 +896,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 25, - "target": 30, + "source": 27, + "target": 32, "overhead": [ { "field": "num_vars", @@ -889,8 +907,8 @@ "doc_path": "rules/maximumsetpacking_qubo/index.html" }, { - "source": 26, - "target": 4, + "source": 28, + "target": 6, "overhead": [ { "field": "num_vars", @@ -904,8 +922,8 @@ "doc_path": "rules/maximumsetpacking_ilp/index.html" }, { - "source": 26, - "target": 19, + "source": 28, + "target": 21, "overhead": [ { "field": "num_vertices", @@ -919,8 +937,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 26, - "target": 25, + "source": 28, + "target": 27, "overhead": [ { "field": "num_sets", @@ -934,8 +952,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 27, - "target": 4, + "source": 29, + "target": 6, "overhead": [ { "field": "num_vars", @@ -949,8 +967,8 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 28, - "target": 4, + "source": 30, + "target": 6, "overhead": [ { "field": "num_vars", @@ -964,8 +982,8 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 29, - "target": 4, + "source": 31, + "target": 6, "overhead": [ { "field": "num_vars", @@ -979,8 +997,8 @@ "doc_path": "rules/minimumvertexcover_ilp/index.html" }, { - "source": 29, - "target": 19, + "source": 31, + "target": 21, "overhead": [ { "field": "num_vertices", @@ -994,8 +1012,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 29, - "target": 28, + "source": 31, + "target": 30, "overhead": [ { "field": "num_sets", @@ -1009,8 +1027,8 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 29, - "target": 30, + "source": 31, + "target": 32, "overhead": [ { "field": "num_vars", @@ -1020,8 +1038,8 @@ "doc_path": "rules/minimumvertexcover_qubo/index.html" }, { - "source": 30, - "target": 4, + "source": 32, + "target": 6, "overhead": [ { "field": "num_vars", @@ -1035,8 +1053,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 30, - "target": 32, + "source": 32, + "target": 34, "overhead": [ { "field": "num_spins", @@ -1046,7 +1064,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 31, + "source": 33, "target": 2, "overhead": [ { @@ -1061,8 +1079,8 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 31, - "target": 6, + "source": 33, + "target": 8, "overhead": [ { "field": "num_vertices", @@ -1076,8 +1094,8 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 31, - "target": 11, + "source": 33, + "target": 13, "overhead": [ { "field": "num_clauses", @@ -1091,8 +1109,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 31, - "target": 18, + "source": 33, + "target": 20, "overhead": [ { "field": "num_vertices", @@ -1106,8 +1124,8 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 31, - "target": 27, + "source": 33, + "target": 29, "overhead": [ { "field": "num_vertices", @@ -1121,8 +1139,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 32, - "target": 30, + "source": 34, + "target": 32, "overhead": [ { "field": "num_vars", @@ -1132,8 +1150,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 33, - "target": 13, + "source": 35, + "target": 15, "overhead": [ { "field": "num_vertices", @@ -1147,8 +1165,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 33, - "target": 32, + "source": 35, + "target": 34, "overhead": [ { "field": "num_spins", @@ -1162,8 +1180,8 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 34, - "target": 4, + "source": 36, + "target": 6, "overhead": [ { "field": "num_vars", diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 43b238dd..877ffb2d 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,5 +1,5 @@ use anyhow::{bail, Context, Result}; -use problemreductions::models::optimization::{BinPacking, ILP}; +use problemreductions::models::optimization::{BinPacking, ClosestVectorProblem, ILP}; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; use problemreductions::solvers::{BruteForce, ILPSolver, Solver}; @@ -239,6 +239,10 @@ pub fn load_problem( Some("f64") => deser_opt::>(data), _ => deser_opt::>(data), }, + "ClosestVectorProblem" => match variant.get("weight").map(|s| s.as_str()) { + Some("f64") => deser_opt::>(data), + _ => deser_opt::>(data), + }, _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } } @@ -294,6 +298,10 @@ pub fn serialize_any_problem( Some("f64") => try_ser::>(any), _ => try_ser::>(any), }, + "ClosestVectorProblem" => match variant.get("weight").map(|s| s.as_str()) { + Some("f64") => try_ser::>(any), + _ => try_ser::>(any), + }, _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } } diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 05f3dec3..43a5f2c4 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -20,6 +20,7 @@ pub const ALIASES: &[(&str, &str)] = &[ ("KSAT", "KSatisfiability"), ("TSP", "TravelingSalesman"), ("BP", "BinPacking"), + ("CVP", "ClosestVectorProblem"), ]; /// Resolve a short alias to the canonical problem name. @@ -49,6 +50,7 @@ pub fn resolve_alias(input: &str) -> String { "bmf" => "BMF".to_string(), "bicliquecover" => "BicliqueCover".to_string(), "bp" | "binpacking" => "BinPacking".to_string(), + "cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(), _ => input.to_string(), // pass-through for exact names } } diff --git a/src/models/mod.rs b/src/models/mod.rs index 1f753b73..e4671efc 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -13,7 +13,7 @@ pub use graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumVertexCover, TravelingSalesman, }; -pub use optimization::{BinPacking, SpinGlass, ILP, QUBO}; +pub use optimization::{BinPacking, ClosestVectorProblem, SpinGlass, ILP, QUBO}; pub use satisfiability::{CNFClause, KSatisfiability, Satisfiability}; pub use set::{MaximumSetPacking, MinimumSetCovering}; pub use specialized::{BicliqueCover, CircuitSAT, Factoring, PaintShop, BMF}; diff --git a/src/models/optimization/closest_vector_problem.rs b/src/models/optimization/closest_vector_problem.rs new file mode 100644 index 00000000..ab3adc7a --- /dev/null +++ b/src/models/optimization/closest_vector_problem.rs @@ -0,0 +1,182 @@ +//! Closest Vector Problem (CVP) implementation. +//! +//! Given a lattice basis B and target vector t, find integer coefficients x +//! minimizing ‖Bx - t‖₂. + +use crate::models::optimization::VarBounds; +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "ClosestVectorProblem", + module_path: module_path!(), + description: "Find the closest lattice point to a target vector", + fields: &[ + FieldInfo { name: "basis", type_name: "Vec>", description: "Basis matrix B as column vectors" }, + FieldInfo { name: "target", type_name: "Vec", description: "Target vector t" }, + FieldInfo { name: "bounds", type_name: "Vec", description: "Integer bounds per variable" }, + ], + } +} + +/// Closest Vector Problem (CVP). +/// +/// Given a lattice basis B ∈ R^{m×n} and target t ∈ R^m, +/// find integer x ∈ Z^n minimizing ‖Bx - t‖₂. +/// +/// Variables are integer coefficients with explicit bounds for enumeration. +/// The configuration encoding follows ILP: config[i] is an offset from bounds[i].lower. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClosestVectorProblem { + /// Basis matrix B stored as n column vectors, each of dimension m. + basis: Vec>, + /// Target vector t ∈ R^m. + target: Vec, + /// Integer bounds per variable for enumeration. + bounds: Vec, +} + +impl ClosestVectorProblem { + /// Create a new CVP instance. + /// + /// # Arguments + /// * `basis` - n column vectors of dimension m + /// * `target` - target vector of dimension m + /// * `bounds` - integer bounds per variable (length n) + /// + /// # Panics + /// Panics if basis/bounds lengths mismatch or dimensions are inconsistent. + pub fn new(basis: Vec>, target: Vec, bounds: Vec) -> Self { + let n = basis.len(); + assert_eq!( + bounds.len(), + n, + "bounds length must match number of basis vectors" + ); + let m = target.len(); + for (i, col) in basis.iter().enumerate() { + assert_eq!( + col.len(), + m, + "basis vector {i} has length {}, expected {m}", + col.len() + ); + } + Self { + basis, + target, + bounds, + } + } + + /// Number of basis vectors (lattice dimension n). + pub fn num_basis_vectors(&self) -> usize { + self.basis.len() + } + + /// Dimension of the ambient space (m). + pub fn ambient_dimension(&self) -> usize { + self.target.len() + } + + /// Access the basis matrix. + pub fn basis(&self) -> &[Vec] { + &self.basis + } + + /// Access the target vector. + pub fn target(&self) -> &[f64] { + &self.target + } + + /// Access the variable bounds. + pub fn bounds(&self) -> &[VarBounds] { + &self.bounds + } + + /// Convert a configuration (offsets from lower bounds) to integer values. + fn config_to_values(&self, config: &[usize]) -> Vec { + config + .iter() + .enumerate() + .map(|(i, &c)| { + let lo = self.bounds.get(i).and_then(|b| b.lower).unwrap_or(0); + lo + c as i64 + }) + .collect() + } +} + +impl Problem for ClosestVectorProblem +where + T: Clone + + Into + + crate::variant::VariantParam + + Serialize + + for<'de> Deserialize<'de> + + std::fmt::Debug + + 'static, +{ + const NAME: &'static str = "ClosestVectorProblem"; + type Metric = SolutionSize; + + fn dims(&self) -> Vec { + self.bounds + .iter() + .map(|b| { + b.num_values().expect( + "CVP brute-force enumeration requires all variables to have finite bounds", + ) + }) + .collect() + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + let values = self.config_to_values(config); + let m = self.ambient_dimension(); + let mut diff = vec![0.0f64; m]; + for (i, &x_i) in values.iter().enumerate() { + for (j, b_ji) in self.basis[i].iter().enumerate() { + diff[j] += x_i as f64 * b_ji.clone().into(); + } + } + for (d, t) in diff.iter_mut().zip(self.target.iter()) { + *d -= t; + } + let norm = diff.iter().map(|d| d * d).sum::().sqrt(); + SolutionSize::Valid(norm) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![T] + } +} + +impl OptimizationProblem for ClosestVectorProblem +where + T: Clone + + Into + + crate::variant::VariantParam + + Serialize + + for<'de> Deserialize<'de> + + std::fmt::Debug + + 'static, +{ + type Value = f64; + + fn direction(&self) -> Direction { + Direction::Minimize + } +} + +crate::declare_variants! { + ClosestVectorProblem => "exp(num_basis_vectors)", + ClosestVectorProblem => "exp(num_basis_vectors)", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/optimization/closest_vector_problem.rs"] +mod tests; diff --git a/src/models/optimization/mod.rs b/src/models/optimization/mod.rs index 6e86fc48..b4feffe0 100644 --- a/src/models/optimization/mod.rs +++ b/src/models/optimization/mod.rs @@ -2,16 +2,19 @@ //! //! This module contains optimization problems: //! - [`BinPacking`]: Bin Packing (minimize bins) +//! - [`ClosestVectorProblem`]: Closest Vector Problem (minimize lattice distance) //! - [`SpinGlass`]: Ising model Hamiltonian //! - [`QUBO`]: Quadratic Unconstrained Binary Optimization //! - [`ILP`]: Integer Linear Programming mod bin_packing; +mod closest_vector_problem; mod ilp; mod qubo; mod spin_glass; pub use bin_packing::BinPacking; +pub use closest_vector_problem::ClosestVectorProblem; pub use ilp::{Comparison, LinearConstraint, ObjectiveSense, VarBounds, ILP}; pub use qubo::QUBO; pub use spin_glass::SpinGlass; diff --git a/src/unit_tests/models/optimization/closest_vector_problem.rs b/src/unit_tests/models/optimization/closest_vector_problem.rs new file mode 100644 index 00000000..4d61d520 --- /dev/null +++ b/src/unit_tests/models/optimization/closest_vector_problem.rs @@ -0,0 +1,178 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; + +#[test] +fn test_cvp_creation() { + // 3D integer lattice: b1=(2,0,0), b2=(1,2,0), b3=(0,1,2) + let basis = vec![vec![2, 0, 0], vec![1, 2, 0], vec![0, 1, 2]]; + let target = vec![3.0, 3.0, 3.0]; + let bounds = vec![ + VarBounds::bounded(-2, 4), + VarBounds::bounded(-2, 4), + VarBounds::bounded(-2, 4), + ]; + let cvp = ClosestVectorProblem::new(basis, target, bounds); + assert_eq!(cvp.num_variables(), 3); + assert_eq!(cvp.ambient_dimension(), 3); + assert_eq!(cvp.num_basis_vectors(), 3); +} + +#[test] +fn test_cvp_evaluate() { + // b1=(2,0,0), b2=(1,2,0), b3=(0,1,2), target=(3,3,3) + let basis = vec![vec![2, 0, 0], vec![1, 2, 0], vec![0, 1, 2]]; + let target = vec![3.0, 3.0, 3.0]; + let bounds = vec![ + VarBounds::bounded(-2, 4), + VarBounds::bounded(-2, 4), + VarBounds::bounded(-2, 4), + ]; + let cvp = ClosestVectorProblem::new(basis, target, bounds); + + // x=(1,1,1) -> Bx=(3,3,2), distance=1.0 + // config offset: x_i - lower = 1 - (-2) = 3 + let config_111 = vec![3, 3, 3]; // maps to x=(1,1,1) + let result = Problem::evaluate(&cvp, &config_111); + assert_eq!(result, SolutionSize::Valid(1.0)); +} + +#[test] +fn test_cvp_direction() { + let basis = vec![vec![1, 0], vec![0, 1]]; + let target = vec![0.5, 0.5]; + let bounds = vec![VarBounds::bounded(0, 2), VarBounds::bounded(0, 2)]; + let cvp = ClosestVectorProblem::new(basis, target, bounds); + assert_eq!(cvp.direction(), Direction::Minimize); +} + +#[test] +fn test_cvp_dims() { + let basis = vec![vec![1, 0], vec![0, 1]]; + let target = vec![0.5, 0.5]; + let bounds = vec![VarBounds::bounded(-1, 3), VarBounds::bounded(0, 5)]; + let cvp = ClosestVectorProblem::new(basis, target, bounds); + assert_eq!(cvp.dims(), vec![5, 6]); // (-1..3)=5 values, (0..5)=6 values +} + +#[test] +fn test_cvp_brute_force() { + // b1=(2,0,0), b2=(1,2,0), b3=(0,1,2), target=(3,3,3) + // Optimal: x=(1,1,1), Bx=(3,3,2), distance=1.0 + let basis = vec![vec![2, 0, 0], vec![1, 2, 0], vec![0, 1, 2]]; + let target = vec![3.0, 3.0, 3.0]; + let bounds = vec![ + VarBounds::bounded(-1, 3), + VarBounds::bounded(-1, 3), + VarBounds::bounded(-1, 3), + ]; + let cvp = ClosestVectorProblem::new(basis, target, bounds); + + let solver = BruteForce::new(); + let solution = solver.find_best(&cvp).expect("should find a solution"); + let values: Vec = solution + .iter() + .enumerate() + .map(|(i, &c)| cvp.bounds()[i].lower.unwrap() + c as i64) + .collect(); + assert_eq!(values, vec![1, 1, 1]); + assert_eq!(Problem::evaluate(&cvp, &solution), SolutionSize::Valid(1.0)); +} + +#[test] +fn test_cvp_serialization() { + let basis = vec![vec![2, 0, 0], vec![1, 2, 0], vec![0, 1, 2]]; + let target = vec![3.0, 3.0, 3.0]; + let bounds = vec![ + VarBounds::bounded(-2, 4), + VarBounds::bounded(-2, 4), + VarBounds::bounded(-2, 4), + ]; + let cvp = ClosestVectorProblem::new(basis, target, bounds); + + let json = serde_json::to_string(&cvp).expect("serialize"); + let cvp2: ClosestVectorProblem = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(cvp2.num_basis_vectors(), 3); + assert_eq!(cvp2.ambient_dimension(), 3); + // Verify functional equivalence after round-trip + let config = vec![3, 3, 3]; + assert_eq!( + Problem::evaluate(&cvp, &config), + Problem::evaluate(&cvp2, &config) + ); +} + +#[test] +fn test_cvp_f64_basis() { + // Non-integer basis to exercise the f64 variant + let basis: Vec> = vec![vec![1.5, 0.0], vec![0.0, 2.0]]; + let target = vec![1.0, 1.0]; + let bounds = vec![VarBounds::bounded(-2, 2), VarBounds::bounded(-2, 2)]; + let cvp = ClosestVectorProblem::new(basis, target, bounds); + + let solver = BruteForce::new(); + let solution = solver.find_best(&cvp).expect("should find a solution"); + let values: Vec = solution + .iter() + .enumerate() + .map(|(i, &c)| cvp.bounds()[i].lower.unwrap() + c as i64) + .collect(); + // x=(1,1): Bx=(1.5, 2.0), dist=sqrt(0.25+1.0)=sqrt(1.25)≈1.118 + // x=(1,0): Bx=(1.5, 0.0), dist=sqrt(0.25+1.0)=sqrt(1.25)≈1.118 + // x=(0,1): Bx=(0.0, 2.0), dist=sqrt(1.0+1.0)=sqrt(2.0)≈1.414 + // x=(0,0): Bx=(0.0, 0.0), dist=sqrt(1.0+1.0)=sqrt(2.0)≈1.414 + // Both (1,0) and (1,1) tie at sqrt(1.25); brute force returns first found + assert!(values == vec![1, 0] || values == vec![1, 1]); +} + +#[test] +fn test_cvp_2d_identity() { + // Identity basis in 2D, target=(0.3, 0.7) + // Closest: x=(0,1), Bx=(0,1), distance=sqrt(0.09+0.09)=0.3*sqrt(2) + let basis = vec![vec![1, 0], vec![0, 1]]; + let target = vec![0.3, 0.7]; + let bounds = vec![VarBounds::bounded(-2, 2), VarBounds::bounded(-2, 2)]; + let cvp = ClosestVectorProblem::new(basis, target, bounds); + + let solver = BruteForce::new(); + let solution = solver.find_best(&cvp).expect("should find a solution"); + let values: Vec = solution + .iter() + .enumerate() + .map(|(i, &c)| cvp.bounds()[i].lower.unwrap() + c as i64) + .collect(); + assert_eq!(values, vec![0, 1]); +} + +#[test] +fn test_cvp_evaluate_exact_solution() { + // Target is exactly a lattice point: t = (2, 2), basis = identity + let basis = vec![vec![1, 0], vec![0, 1]]; + let target = vec![2.0, 2.0]; + let bounds = vec![VarBounds::bounded(0, 4), VarBounds::bounded(0, 4)]; + let cvp = ClosestVectorProblem::new(basis, target, bounds); + + // x=(2,2), Bx=(2,2), distance=0 + let config = vec![2, 2]; // offset from lower=0 + let result = Problem::evaluate(&cvp, &config); + assert_eq!(result, SolutionSize::Valid(0.0)); +} + +#[test] +#[should_panic(expected = "bounds length must match")] +fn test_cvp_mismatched_bounds() { + let basis = vec![vec![1, 0], vec![0, 1]]; + let target = vec![0.5, 0.5]; + let bounds = vec![VarBounds::bounded(0, 1)]; // only 1 bound for 2 vars + ClosestVectorProblem::new(basis, target, bounds); +} + +#[test] +#[should_panic(expected = "basis vector")] +fn test_cvp_inconsistent_dimensions() { + let basis = vec![vec![1, 0], vec![0]]; // second vector has wrong dim + let target = vec![0.5, 0.5]; + let bounds = vec![VarBounds::bounded(0, 1), VarBounds::bounded(0, 1)]; + ClosestVectorProblem::new(basis, target, bounds); +}