From 00ce084dbde4e68f4d69fc28f90b153c1ddc672f Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Feb 2026 14:30:20 +0800 Subject: [PATCH 1/7] Add plan for #90: ClosestVectorProblem model Co-Authored-By: Claude Opus 4.6 --- .../2026-02-22-closest-vector-problem.md | 557 ++++++++++++++++++ 1 file changed, 557 insertions(+) create mode 100644 docs/plans/2026-02-22-closest-vector-problem.md diff --git a/docs/plans/2026-02-22-closest-vector-problem.md b/docs/plans/2026-02-22-closest-vector-problem.md new file mode 100644 index 00000000..2a1c4f45 --- /dev/null +++ b/docs/plans/2026-02-22-closest-vector-problem.md @@ -0,0 +1,557 @@ +# ClosestVectorProblem Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the Closest Vector Problem (CVP) as a new optimization model, following the `add-model` skill steps 1-7. + +**Architecture:** CVP is a lattice problem parameterized by element type `T` (i32 or f64). It minimizes ‖Bx - t‖₂ over integer vectors x with explicit bounds per variable. The struct stores the basis matrix, target vector, and variable bounds (reusing `VarBounds` from ILP). Configuration encoding follows the ILP pattern: config indices are offsets from lower bounds. + +**Tech Stack:** Rust, serde, inventory crate for schema registration + +--- + +### Task 1: Create the CVP model file with struct and constructor + +**Files:** +- Create: `src/models/optimization/closest_vector_problem.rs` + +**Step 1: Write the failing test** + +Create `src/unit_tests/models/optimization/closest_vector_problem.rs`: + +```rust +use super::*; +use crate::models::optimization::VarBounds; +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); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test test_cvp_creation -- --no-capture 2>&1 | tail -20` +Expected: FAIL (module not found) + +**Step 3: Write minimal implementation** + +Create `src/models/optimization/closest_vector_problem.rs` with: + +```rust +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() + } +} +``` + +**Step 4: Run test to verify it passes** + +Run: `cargo test test_cvp_creation -- --no-capture 2>&1 | tail -20` +Expected: FAIL (not yet registered in mod.rs — will pass after Task 2) + +**Step 5: Commit** + +```bash +git add src/models/optimization/closest_vector_problem.rs src/unit_tests/models/optimization/closest_vector_problem.rs +git commit -m "feat: add ClosestVectorProblem struct and constructor" +``` + +--- + +### Task 2: Implement Problem and OptimizationProblem traits + +**Files:** +- Modify: `src/models/optimization/closest_vector_problem.rs` + +**Step 1: Write the failing tests** + +Add to `src/unit_tests/models/optimization/closest_vector_problem.rs`: + +```rust +#[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 +} +``` + +**Step 2: Run tests to verify they fail** + +Run: `cargo test test_cvp -- --no-capture 2>&1 | tail -20` +Expected: FAIL (trait not implemented) + +**Step 3: Implement the traits** + +Add to `src/models/optimization/closest_vector_problem.rs`, requiring `T: Into + Clone`: + +```rust +impl + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug + 'static> Problem for ClosestVectorProblem { + 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); + // Compute Bx - t, then ‖Bx - t‖₂ + let m = self.ambient_dimension(); + let mut diff = vec![0.0f64; m]; + // Bx = sum_i x_i * basis[i] + 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(); + } + } + // diff = Bx - t + for j in 0..m { + diff[j] -= self.target[j]; + } + let norm = diff.iter().map(|d| d * d).sum::().sqrt(); + SolutionSize::Valid(norm) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn problem_size_names() -> &'static [&'static str] { + &["num_basis_vectors", "ambient_dimension"] + } + + fn problem_size_values(&self) -> Vec { + vec![self.num_basis_vectors(), self.ambient_dimension()] + } +} + +impl + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug + 'static> OptimizationProblem for ClosestVectorProblem { + type Value = f64; + + fn direction(&self) -> Direction { + Direction::Minimize + } +} + +#[cfg(test)] +#[path = "../../unit_tests/models/optimization/closest_vector_problem.rs"] +mod tests; +``` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test test_cvp -- --no-capture 2>&1 | tail -20` +Expected: PASS (all 4 tests) + +**Step 5: Commit** + +```bash +git add src/models/optimization/closest_vector_problem.rs src/unit_tests/models/optimization/closest_vector_problem.rs +git commit -m "feat: implement Problem and OptimizationProblem traits for CVP" +``` + +--- + +### Task 3: Register CVP in module exports and prelude + +**Files:** +- Modify: `src/models/optimization/mod.rs` +- Modify: `src/models/mod.rs` + +**Step 1: Update `src/models/optimization/mod.rs`** + +Add module declaration and re-export: + +```rust +mod closest_vector_problem; +// add to existing pub use line: +pub use closest_vector_problem::ClosestVectorProblem; +``` + +**Step 2: Update `src/models/mod.rs`** + +Add `ClosestVectorProblem` to the optimization re-export line: + +```rust +pub use optimization::{ClosestVectorProblem, SpinGlass, ILP, QUBO}; +``` + +**Step 3: Verify compilation** + +Run: `cargo build 2>&1 | tail -20` +Expected: successful compilation + +**Step 4: Commit** + +```bash +git add src/models/optimization/mod.rs src/models/mod.rs +git commit -m "feat: register ClosestVectorProblem in module exports" +``` + +--- + +### Task 4: Register CVP in CLI dispatch + +**Files:** +- Modify: `problemreductions-cli/src/dispatch.rs` +- Modify: `problemreductions-cli/src/problem_name.rs` + +**Step 1: Update `problemreductions-cli/src/dispatch.rs`** + +Add import at top: +```rust +use problemreductions::models::optimization::ClosestVectorProblem; +``` + +Add match arm in `load_problem()` (after the `"ILP"` arm): +```rust +"ClosestVectorProblem" => deser_opt::>(data), +``` + +Add match arm in `serialize_any_problem()` (after the `"ILP"` arm): +```rust +"ClosestVectorProblem" => try_ser::>(data), +``` + +**Step 2: Update `problemreductions-cli/src/problem_name.rs`** + +Add alias in `resolve_alias()`: +```rust +"closestvectorproblem" | "cvp" => "ClosestVectorProblem".to_string(), +``` + +Add to `ALIASES` array: +```rust +("CVP", "ClosestVectorProblem"), +``` + +**Step 3: Verify CLI builds** + +Run: `cargo build -p problemreductions-cli 2>&1 | tail -20` +Expected: successful build + +**Step 4: Commit** + +```bash +git add problemreductions-cli/src/dispatch.rs problemreductions-cli/src/problem_name.rs +git commit -m "feat: register ClosestVectorProblem in CLI dispatch" +``` + +--- + +### Task 5: Write comprehensive unit tests + +**Files:** +- Modify: `src/unit_tests/models/optimization/closest_vector_problem.rs` + +**Step 1: Add solver and serialization tests** + +Append to the test file: + +```rust +use crate::solvers::BruteForce; + +#[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]); +} + +#[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); +} + +#[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_problem_size() { + let basis = vec![vec![1, 0, 0], vec![0, 1, 0]]; // 2 vectors in R^3 + let target = vec![0.5, 0.5, 0.5]; + let bounds = vec![VarBounds::bounded(0, 2), VarBounds::bounded(0, 2)]; + let cvp = ClosestVectorProblem::new(basis, target, bounds); + assert_eq!(ClosestVectorProblem::::problem_size_names(), &["num_basis_vectors", "ambient_dimension"]); + assert_eq!(cvp.problem_size_values(), vec![2, 3]); +} + +#[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); +} +``` + +**Step 2: Run all tests** + +Run: `cargo test test_cvp -- --no-capture 2>&1 | tail -30` +Expected: PASS (all tests) + +**Step 3: Commit** + +```bash +git add src/unit_tests/models/optimization/closest_vector_problem.rs +git commit -m "test: add comprehensive CVP unit tests" +``` + +--- + +### Task 6: Add paper documentation + +**Files:** +- Modify: `docs/paper/reductions.typ` + +**Step 1: Add display name** + +Add to the `display-name` dictionary (after the `"BicliqueCover"` entry): + +```typst +"ClosestVectorProblem": [Closest Vector Problem], +``` + +**Step 2: Add problem definition** + +Add a `#problem-def` block (after the ILP definition, in the optimization section): + +```typst +#problem-def("ClosestVectorProblem")[ + Given a lattice basis $bold(B) in RR^(m times n)$ (columns $bold(b)_1, ..., 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$. +] +``` + +**Step 3: Verify paper builds** + +Run: `make doc 2>&1 | tail -10` +Expected: successful build (warnings about missing reductions are OK for a new problem with no reduction rules yet) + +**Step 4: Commit** + +```bash +git add docs/paper/reductions.typ +git commit -m "docs: add ClosestVectorProblem definition to paper" +``` + +--- + +### Task 7: Final verification + +**Step 1: Run full check** + +Run: `make test clippy` +Expected: all tests pass, no clippy warnings + +**Step 2: Run review-implementation skill** + +Verify all structural checks pass for the new model. + +**Step 3: Squash or tidy commits if needed** + +Ensure all commits are clean and ready for PR. From b72048122b12ec8346cee63cd2abb5a6f6161345 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 28 Feb 2026 22:29:51 +0800 Subject: [PATCH 2/7] docs: update CVP plan for refactored variant system and Copilot review Changes from original plan: - Remove problem_size_names/values (removed from Problem trait) - Add declare_variants! with complexity metadata for i32/f64 variants - Add VariantParam trait bound on T parameter - Use variant_params![T] instead of variant_params![] - CLI dispatch supports both i32 and f64 via variant map (SpinGlass pattern) - Remove redundant VarBounds import in tests (super::* suffices) Co-Authored-By: Claude Opus 4.6 --- .../2026-02-22-closest-vector-problem.md | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/docs/plans/2026-02-22-closest-vector-problem.md b/docs/plans/2026-02-22-closest-vector-problem.md index 2a1c4f45..4b4667ea 100644 --- a/docs/plans/2026-02-22-closest-vector-problem.md +++ b/docs/plans/2026-02-22-closest-vector-problem.md @@ -21,7 +21,6 @@ Create `src/unit_tests/models/optimization/closest_vector_problem.rs`: ```rust use super::*; -use crate::models::optimization::VarBounds; use crate::traits::{OptimizationProblem, Problem}; use crate::types::{Direction, SolutionSize}; @@ -161,7 +160,7 @@ git commit -m "feat: add ClosestVectorProblem struct and constructor" --- -### Task 2: Implement Problem and OptimizationProblem traits +### Task 2: Implement Problem and OptimizationProblem traits with declare_variants! **Files:** - Modify: `src/models/optimization/closest_vector_problem.rs` @@ -216,10 +215,15 @@ Expected: FAIL (trait not implemented) **Step 3: Implement the traits** -Add to `src/models/optimization/closest_vector_problem.rs`, requiring `T: Into + Clone`: +Add to `src/models/optimization/closest_vector_problem.rs`. + +Note: `T` must have `crate::variant::VariantParam` trait bound (category "weight") for the variant system to work. Follow the SpinGlass pattern. ```rust -impl + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug + 'static> Problem for ClosestVectorProblem { +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; @@ -236,16 +240,13 @@ impl + Serialize + for<'de> Deserialize<'de> + std::fmt::De fn evaluate(&self, config: &[usize]) -> SolutionSize { let values = self.config_to_values(config); - // Compute Bx - t, then ‖Bx - t‖₂ let m = self.ambient_dimension(); let mut diff = vec![0.0f64; m]; - // Bx = sum_i x_i * basis[i] 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(); } } - // diff = Bx - t for j in 0..m { diff[j] -= self.target[j]; } @@ -254,19 +255,14 @@ impl + Serialize + for<'de> Deserialize<'de> + std::fmt::De } fn variant() -> Vec<(&'static str, &'static str)> { - crate::variant_params![] - } - - fn problem_size_names() -> &'static [&'static str] { - &["num_basis_vectors", "ambient_dimension"] - } - - fn problem_size_values(&self) -> Vec { - vec![self.num_basis_vectors(), self.ambient_dimension()] + crate::variant_params![T] } } -impl + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug + 'static> OptimizationProblem for ClosestVectorProblem { +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 { @@ -274,11 +270,22 @@ impl + Serialize + for<'de> Deserialize<'de> + std::fmt::De } } +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; ``` +**Notes on changes from original plan:** +- **Added `crate::variant::VariantParam` trait bound** on `T` (required by refactored variant system, per Copilot review) +- **Changed `variant_params![]` to `variant_params![T]`** since CVP is parameterized by element type `T` which maps to "weight" category (per Copilot review) +- **Removed `problem_size_names()` / `problem_size_values()`** — these methods were removed from the `Problem` trait. Size getters are now inherent methods only (already have `num_basis_vectors()` and `ambient_dimension()`) +- **Added `declare_variants!`** block registering both `i32` and `f64` concrete variants with complexity metadata. CVP complexity is `exp(num_basis_vectors)` — exact CVP is NP-hard under randomized reductions and the best known exact algorithms have exponential complexity in the lattice dimension + **Step 4: Run tests to verify they pass** Run: `cargo test test_cvp -- --no-capture 2>&1 | tail -20` @@ -344,14 +351,20 @@ Add import at top: use problemreductions::models::optimization::ClosestVectorProblem; ``` -Add match arm in `load_problem()` (after the `"ILP"` arm): +Add match arm in `load_problem()` (after the `"ILP"` arm), supporting both i32 and f64 via variant map (following SpinGlass pattern, per Copilot review): ```rust -"ClosestVectorProblem" => deser_opt::>(data), +"ClosestVectorProblem" => match variant.get("weight").map(|s| s.as_str()) { + Some("f64") => deser_opt::>(data), + _ => deser_opt::>(data), +}, ``` -Add match arm in `serialize_any_problem()` (after the `"ILP"` arm): +Add match arm in `serialize_any_problem()` (after the `"ILP"` arm), same pattern (per Copilot review): ```rust -"ClosestVectorProblem" => try_ser::>(data), +"ClosestVectorProblem" => match variant.get("weight").map(|s| s.as_str()) { + Some("f64") => try_ser::>(any), + _ => try_ser::>(any), +}, ``` **Step 2: Update `problemreductions-cli/src/problem_name.rs`** @@ -447,16 +460,6 @@ fn test_cvp_2d_identity() { assert_eq!(values, vec![0, 1]); } -#[test] -fn test_cvp_problem_size() { - let basis = vec![vec![1, 0, 0], vec![0, 1, 0]]; // 2 vectors in R^3 - let target = vec![0.5, 0.5, 0.5]; - let bounds = vec![VarBounds::bounded(0, 2), VarBounds::bounded(0, 2)]; - let cvp = ClosestVectorProblem::new(basis, target, bounds); - assert_eq!(ClosestVectorProblem::::problem_size_names(), &["num_basis_vectors", "ambient_dimension"]); - assert_eq!(cvp.problem_size_values(), vec![2, 3]); -} - #[test] fn test_cvp_evaluate_exact_solution() { // Target is exactly a lattice point: t = (2, 2), basis = identity @@ -529,7 +532,7 @@ Add a `#problem-def` block (after the ILP definition, in the optimization sectio **Step 3: Verify paper builds** -Run: `make doc 2>&1 | tail -10` +Run: `make paper 2>&1 | tail -10` Expected: successful build (warnings about missing reductions are OK for a new problem with no reduction rules yet) **Step 4: Commit** From 9827a1e2328a6f3581ff4e493def2487ee689e6c Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 28 Feb 2026 23:46:51 +0800 Subject: [PATCH 3/7] feat: add ClosestVectorProblem model (#90) Add CVP as a new optimization model parameterized by element type T (i32/f64). Implements Problem and OptimizationProblem traits, registers in CLI dispatch with "CVP" alias, and adds problem definition to paper. Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 9 + problemreductions-cli/src/dispatch.rs | 10 +- problemreductions-cli/src/problem_name.rs | 2 + src/models/mod.rs | 2 +- .../optimization/closest_vector_problem.rs | 182 ++++++++++++++++++ src/models/optimization/mod.rs | 3 + .../optimization/closest_vector_problem.rs | 178 +++++++++++++++++ 7 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 src/models/optimization/closest_vector_problem.rs create mode 100644 src/unit_tests/models/optimization/closest_vector_problem.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 07112ff0..ac9c4f94 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,14 @@ 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 plays a central role in lattice-based cryptography and the geometry of numbers. Kannan's algorithm @kannan1987 solves CVP in $O^*(n^n)$ time using the Hermite normal form, later improved to $O^*(2^n)$ via the randomized sieve of Micciancio and Voulgaris @micciancio2010. CVP is closely related to the Shortest Vector Problem (SVP) and integer programming: Lenstra's algorithm for fixed-dimensional ILP @lenstra1983 proceeds via CVP in the dual lattice. + + *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)(1, 1)^top = (3, 2)^top$, and $bold(B)(0, 1)^top = (1, 2)^top$. The closest is $bold(B)(1, 1)^top = (3, 2)^top$ with distance $norm((0.2, 0.5))_2 approx 0.539$. +] + == Satisfiability Problems #problem-def("Satisfiability")[ 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); +} From c6dc4b996ae4b8391f030cea8b6dfeea8d90bea4 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 28 Feb 2026 23:59:46 +0800 Subject: [PATCH 4/7] chore: remove completed CVP implementation plan Co-Authored-By: Claude Opus 4.6 --- .../2026-02-22-closest-vector-problem.md | 560 ------------------ 1 file changed, 560 deletions(-) delete mode 100644 docs/plans/2026-02-22-closest-vector-problem.md diff --git a/docs/plans/2026-02-22-closest-vector-problem.md b/docs/plans/2026-02-22-closest-vector-problem.md deleted file mode 100644 index 4b4667ea..00000000 --- a/docs/plans/2026-02-22-closest-vector-problem.md +++ /dev/null @@ -1,560 +0,0 @@ -# ClosestVectorProblem Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the Closest Vector Problem (CVP) as a new optimization model, following the `add-model` skill steps 1-7. - -**Architecture:** CVP is a lattice problem parameterized by element type `T` (i32 or f64). It minimizes ‖Bx - t‖₂ over integer vectors x with explicit bounds per variable. The struct stores the basis matrix, target vector, and variable bounds (reusing `VarBounds` from ILP). Configuration encoding follows the ILP pattern: config indices are offsets from lower bounds. - -**Tech Stack:** Rust, serde, inventory crate for schema registration - ---- - -### Task 1: Create the CVP model file with struct and constructor - -**Files:** -- Create: `src/models/optimization/closest_vector_problem.rs` - -**Step 1: Write the failing test** - -Create `src/unit_tests/models/optimization/closest_vector_problem.rs`: - -```rust -use super::*; -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); -} -``` - -**Step 2: Run test to verify it fails** - -Run: `cargo test test_cvp_creation -- --no-capture 2>&1 | tail -20` -Expected: FAIL (module not found) - -**Step 3: Write minimal implementation** - -Create `src/models/optimization/closest_vector_problem.rs` with: - -```rust -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() - } -} -``` - -**Step 4: Run test to verify it passes** - -Run: `cargo test test_cvp_creation -- --no-capture 2>&1 | tail -20` -Expected: FAIL (not yet registered in mod.rs — will pass after Task 2) - -**Step 5: Commit** - -```bash -git add src/models/optimization/closest_vector_problem.rs src/unit_tests/models/optimization/closest_vector_problem.rs -git commit -m "feat: add ClosestVectorProblem struct and constructor" -``` - ---- - -### Task 2: Implement Problem and OptimizationProblem traits with declare_variants! - -**Files:** -- Modify: `src/models/optimization/closest_vector_problem.rs` - -**Step 1: Write the failing tests** - -Add to `src/unit_tests/models/optimization/closest_vector_problem.rs`: - -```rust -#[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 -} -``` - -**Step 2: Run tests to verify they fail** - -Run: `cargo test test_cvp -- --no-capture 2>&1 | tail -20` -Expected: FAIL (trait not implemented) - -**Step 3: Implement the traits** - -Add to `src/models/optimization/closest_vector_problem.rs`. - -Note: `T` must have `crate::variant::VariantParam` trait bound (category "weight") for the variant system to work. Follow the SpinGlass pattern. - -```rust -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 j in 0..m { - diff[j] -= self.target[j]; - } - 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; -``` - -**Notes on changes from original plan:** -- **Added `crate::variant::VariantParam` trait bound** on `T` (required by refactored variant system, per Copilot review) -- **Changed `variant_params![]` to `variant_params![T]`** since CVP is parameterized by element type `T` which maps to "weight" category (per Copilot review) -- **Removed `problem_size_names()` / `problem_size_values()`** — these methods were removed from the `Problem` trait. Size getters are now inherent methods only (already have `num_basis_vectors()` and `ambient_dimension()`) -- **Added `declare_variants!`** block registering both `i32` and `f64` concrete variants with complexity metadata. CVP complexity is `exp(num_basis_vectors)` — exact CVP is NP-hard under randomized reductions and the best known exact algorithms have exponential complexity in the lattice dimension - -**Step 4: Run tests to verify they pass** - -Run: `cargo test test_cvp -- --no-capture 2>&1 | tail -20` -Expected: PASS (all 4 tests) - -**Step 5: Commit** - -```bash -git add src/models/optimization/closest_vector_problem.rs src/unit_tests/models/optimization/closest_vector_problem.rs -git commit -m "feat: implement Problem and OptimizationProblem traits for CVP" -``` - ---- - -### Task 3: Register CVP in module exports and prelude - -**Files:** -- Modify: `src/models/optimization/mod.rs` -- Modify: `src/models/mod.rs` - -**Step 1: Update `src/models/optimization/mod.rs`** - -Add module declaration and re-export: - -```rust -mod closest_vector_problem; -// add to existing pub use line: -pub use closest_vector_problem::ClosestVectorProblem; -``` - -**Step 2: Update `src/models/mod.rs`** - -Add `ClosestVectorProblem` to the optimization re-export line: - -```rust -pub use optimization::{ClosestVectorProblem, SpinGlass, ILP, QUBO}; -``` - -**Step 3: Verify compilation** - -Run: `cargo build 2>&1 | tail -20` -Expected: successful compilation - -**Step 4: Commit** - -```bash -git add src/models/optimization/mod.rs src/models/mod.rs -git commit -m "feat: register ClosestVectorProblem in module exports" -``` - ---- - -### Task 4: Register CVP in CLI dispatch - -**Files:** -- Modify: `problemreductions-cli/src/dispatch.rs` -- Modify: `problemreductions-cli/src/problem_name.rs` - -**Step 1: Update `problemreductions-cli/src/dispatch.rs`** - -Add import at top: -```rust -use problemreductions::models::optimization::ClosestVectorProblem; -``` - -Add match arm in `load_problem()` (after the `"ILP"` arm), supporting both i32 and f64 via variant map (following SpinGlass pattern, per Copilot review): -```rust -"ClosestVectorProblem" => match variant.get("weight").map(|s| s.as_str()) { - Some("f64") => deser_opt::>(data), - _ => deser_opt::>(data), -}, -``` - -Add match arm in `serialize_any_problem()` (after the `"ILP"` arm), same pattern (per Copilot review): -```rust -"ClosestVectorProblem" => match variant.get("weight").map(|s| s.as_str()) { - Some("f64") => try_ser::>(any), - _ => try_ser::>(any), -}, -``` - -**Step 2: Update `problemreductions-cli/src/problem_name.rs`** - -Add alias in `resolve_alias()`: -```rust -"closestvectorproblem" | "cvp" => "ClosestVectorProblem".to_string(), -``` - -Add to `ALIASES` array: -```rust -("CVP", "ClosestVectorProblem"), -``` - -**Step 3: Verify CLI builds** - -Run: `cargo build -p problemreductions-cli 2>&1 | tail -20` -Expected: successful build - -**Step 4: Commit** - -```bash -git add problemreductions-cli/src/dispatch.rs problemreductions-cli/src/problem_name.rs -git commit -m "feat: register ClosestVectorProblem in CLI dispatch" -``` - ---- - -### Task 5: Write comprehensive unit tests - -**Files:** -- Modify: `src/unit_tests/models/optimization/closest_vector_problem.rs` - -**Step 1: Add solver and serialization tests** - -Append to the test file: - -```rust -use crate::solvers::BruteForce; - -#[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]); -} - -#[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); -} - -#[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); -} -``` - -**Step 2: Run all tests** - -Run: `cargo test test_cvp -- --no-capture 2>&1 | tail -30` -Expected: PASS (all tests) - -**Step 3: Commit** - -```bash -git add src/unit_tests/models/optimization/closest_vector_problem.rs -git commit -m "test: add comprehensive CVP unit tests" -``` - ---- - -### Task 6: Add paper documentation - -**Files:** -- Modify: `docs/paper/reductions.typ` - -**Step 1: Add display name** - -Add to the `display-name` dictionary (after the `"BicliqueCover"` entry): - -```typst -"ClosestVectorProblem": [Closest Vector Problem], -``` - -**Step 2: Add problem definition** - -Add a `#problem-def` block (after the ILP definition, in the optimization section): - -```typst -#problem-def("ClosestVectorProblem")[ - Given a lattice basis $bold(B) in RR^(m times n)$ (columns $bold(b)_1, ..., 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$. -] -``` - -**Step 3: Verify paper builds** - -Run: `make paper 2>&1 | tail -10` -Expected: successful build (warnings about missing reductions are OK for a new problem with no reduction rules yet) - -**Step 4: Commit** - -```bash -git add docs/paper/reductions.typ -git commit -m "docs: add ClosestVectorProblem definition to paper" -``` - ---- - -### Task 7: Final verification - -**Step 1: Run full check** - -Run: `make test clippy` -Expected: all tests pass, no clippy warnings - -**Step 2: Run review-implementation skill** - -Verify all structural checks pass for the new model. - -**Step 3: Squash or tidy commits if needed** - -Ensure all commits are clean and ready for PR. From 407f7b33def59750147599c92bdee753e64b5b57 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 1 Mar 2026 00:16:38 +0800 Subject: [PATCH 5/7] fix: improve CVP paper entry and add missing citations - Fix factual errors: Kannan runs in n^O(n) (not n^n), Micciancio-Voulgaris is deterministic O*(4^n) Voronoi cell (not randomized sieve O*(2^n)) - Add Aggarwal-Dadush-Stephens-Davidowitz 2015 citation for the O*(2^n) bound - Add BibTeX entries: vanemde1981, kannan1987, micciancio2010, aggarwal2015 - Add lattice visualization figure with basis vectors, target, and closest point - Add evaluation walkthrough showing distance computation - Fix Makefile: mkdir -p tests/julia before rust-export - Regenerate problem_schemas.json and reduction_graph.json Co-Authored-By: Claude Opus 4.6 --- Makefile | 1 + docs/paper/reductions.typ | 38 +++- docs/paper/references.bib | 37 ++++ docs/src/reductions/problem_schemas.json | 21 ++ docs/src/reductions/reduction_graph.json | 242 ++++++++++++----------- 5 files changed, 224 insertions(+), 115 deletions(-) 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 ac9c4f94..9b677592 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -641,9 +641,41 @@ 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 plays a central role in lattice-based cryptography and the geometry of numbers. Kannan's algorithm @kannan1987 solves CVP in $O^*(n^n)$ time using the Hermite normal form, later improved to $O^*(2^n)$ via the randomized sieve of Micciancio and Voulgaris @micciancio2010. CVP is closely related to the Shortest Vector Problem (SVP) and integer programming: Lenstra's algorithm for fixed-dimensional ILP @lenstra1983 proceeds via CVP in the dual lattice. - - *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)(1, 1)^top = (3, 2)^top$, and $bold(B)(0, 1)^top = (1, 2)^top$. The closest is $bold(B)(1, 1)^top = (3, 2)^top$ with distance $norm((0.2, 0.5))_2 approx 0.539$. + 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, { + // 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) + draw.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) }, + ) + } + } + // Target vector + draw.circle((2.8, 1.5), radius: 0.1, fill: graph-colors.at(1), stroke: none) + draw.content((2.8, 1.05), text(7pt)[$bold(t)$]) + // Dashed line from target to closest point + draw.line((2.8, 1.5), (3, 2), stroke: stroke(paint: graph-colors.at(0), thickness: 0.8pt, dash: "dashed")) + // Basis vectors as arrows from origin + draw.line((0, 0), (2, 0), mark: (end: ">"), stroke: 0.8pt + luma(100)) + draw.content((1.0, -0.35), text(7pt)[$bold(b)_1$]) + draw.line((0, 0), (1, 2), mark: (end: ">"), stroke: 0.8pt + luma(100)) + draw.content((0.2, 1.1), text(7pt)[$bold(b)_2$]) + // Label closest point + draw.content((3.45, 2.3), 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 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", From 5481e29abf6e4f089a8dc4a8da5386ece2516bd5 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 1 Mar 2026 00:46:27 +0800 Subject: [PATCH 6/7] save --- docs/paper/reductions.typ | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 9b677592..9460a804 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -647,32 +647,35 @@ Integer Linear Programming is a universal modeling framework: virtually every NP #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) - draw.circle( + 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 - draw.circle((2.8, 1.5), radius: 0.1, fill: graph-colors.at(1), stroke: none) - draw.content((2.8, 1.05), text(7pt)[$bold(t)$]) + 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 - draw.line((2.8, 1.5), (3, 2), stroke: stroke(paint: graph-colors.at(0), thickness: 0.8pt, dash: "dashed")) + line("target", "p11", stroke: (paint: graph-colors.at(0), thickness: 0.8pt, dash: "dashed")) // Basis vectors as arrows from origin - draw.line((0, 0), (2, 0), mark: (end: ">"), stroke: 0.8pt + luma(100)) - draw.content((1.0, -0.35), text(7pt)[$bold(b)_1$]) - draw.line((0, 0), (1, 2), mark: (end: ">"), stroke: 0.8pt + luma(100)) - draw.content((0.2, 1.1), text(7pt)[$bold(b)_2$]) + 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 - draw.content((3.45, 2.3), text(7pt)[$bold(B)(1,1)^top$]) + 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$.], ) From 1d7372e54ff3c55bee5f4b7c9d86e92b5b9237e2 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 1 Mar 2026 00:51:17 +0800 Subject: [PATCH 7/7] chore: add Typst drawing rule for CeTZ conventions Co-Authored-By: Claude Opus 4.6 --- .claude/rules/typst-drawing.md | 91 ++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 .claude/rules/typst-drawing.md 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%))`